Initial commit for homaroma
No files matched your search
@@ -0,0 +1,86 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun.lockb
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc gitignore
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#testing)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (http://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1.x command line compiler build cache
|
||||
third-party/
|
||||
|
||||
# Monorepo build outputs
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
.nuxt/
|
||||
out/
|
||||
.vercel/
|
||||
.output/
|
||||
|
||||
# IDE files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS metadata
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.env
|
||||
|
||||
# SQLite
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Prisma
|
||||
/apps/backend/prisma/dev.db
|
||||
|
||||
# Bun
|
||||
.bun-build
|
||||
@@ -0,0 +1 @@
|
||||
ag_d909195cfceb49ef9f965aaf59b80003
|
||||
@@ -0,0 +1,251 @@
|
||||
# Prisma 7 — คู่มือการเชื่อมต่อและทำ Seed (ไม่งงอีกต่อไป)
|
||||
|
||||
xx
|
||||
> **สำคัญมาก:** Prisma 7 เปลี่ยนวิธีการเชื่อมต่อฐานข้อมูลอย่างพื้นฐาน
|
||||
> ถ้าเคยใช้ Prisma 4, 5, หรือ 6 มาก่อน — ของเก่าพวกนั้น **ใช้ไม่ได้อีกแล้ว**
|
||||
|
||||
---
|
||||
|
||||
## 1. สิ่งที่เปลี่ยนไปจาก Prisma 6fdss → Prisma 7
|
||||
|
||||
| เรื่อง | Prisma 5/6 (เก่า) | Prisma 7 (ใหม่) |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` ใน `schema.prisma` | ✅ ใช้ได้ | ❌ ใช้ไม่ได้ |
|
||||
| `new PrismaClient()` ไม่มี options | ✅ อ่าน `.env` เอง | ❌ Error ทันที |
|
||||
| Config ฐานข้อมูล | อยู่ใน `schema.prisma` | ย้ายไปที่ `prisma.config.ts` |
|
||||
| วิธีเชื่อมต่อ | ผ่าน URL ใน `.env` | ผ่าน **Driver Adapter** |
|
||||
|
||||
---
|
||||
|
||||
## 2. โครงสร้างไฟล์ที่ถูกต้องสำหรับ Prisma 7
|
||||
|
||||
```
|
||||
apps/backend/
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Models เท่านั้น — ห้ามใส่ url ใน datasource
|
||||
│ ├── seed.js # Seed file (ใช้ .js ไม่ใช่ .ts เพื่อหลีกเลี่ยง ts-node issues)
|
||||
│ └── migrations/ # Migration files
|
||||
├── prisma.config.ts # ← จุดตั้งค่าหลักของ Prisma 7
|
||||
├── .env # DATABASE_URL อยู่ที่นี่
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. `schema.prisma` — ห้ามใส่ `url`
|
||||
|
||||
```prisma
|
||||
// ✅ CORRECT — Prisma 7
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
// ❌ ห้ามใส่บรรทัดนี้: url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Scent {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
size String?
|
||||
sku String? @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. `prisma.config.ts` — ไฟล์หลักของ Prisma 7
|
||||
|
||||
```typescript
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "node prisma/seed.js", // ← ชี้ไปที่ seed.js (ไม่ใช่ seed.ts)
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"], // ← URL อยู่ที่นี่แทน schema.prisma
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `seed.js` — Template ที่ใช้งานได้จริง
|
||||
|
||||
> **ทำไมใช้ `.js` ไม่ใช่ `.ts`:** `ts-node` ใน environment นี้ไม่แสดง output ทำให้ debug ยาก ใช้ `.js` แทนเพื่อความเสถียร
|
||||
|
||||
```javascript
|
||||
// seed.js — CommonJS format
|
||||
const path = require('path');
|
||||
|
||||
// โหลด .env ก่อนทุกอย่าง
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaPg } = require('@prisma/adapter-pg'); // ← จำเป็นสำหรับ Prisma 7
|
||||
|
||||
// ✅ วิธีเดียวที่ถูกต้องใน Prisma 7 — ต้องส่ง adapter เข้าไป
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const data = [
|
||||
// ใส่ข้อมูลที่ต้องการ seed ที่นี่
|
||||
{ name: 'ตัวอย่าง', size: '120ml', sku: '120-ตัวอย่าง-120ml' },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
process.stdout.write('🚀 Starting seeding...\n');
|
||||
|
||||
for (const item of data) {
|
||||
await prisma.scent.upsert({
|
||||
where: { sku: item.sku }, // ← ใช้ unique field
|
||||
update: {}, // ← ถ้ามีอยู่แล้วไม่ต้อง update
|
||||
create: item,
|
||||
});
|
||||
process.stdout.write(`✅ ${item.name} (${item.size})\n`);
|
||||
}
|
||||
|
||||
process.stdout.write(`✨ Done! Total: ${data.length} records\n`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
process.stderr.write(`❌ Error: ${e.message}\n`);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Packages ที่ต้อง Install
|
||||
|
||||
```bash
|
||||
# Dependencies หลัก
|
||||
npm i @prisma/client @prisma/adapter-pg pg
|
||||
|
||||
# Dev dependencies
|
||||
npm i -D prisma dotenv
|
||||
```
|
||||
|
||||
> **ต่างจากเดิม:** Prisma 7 ต้องการ `@prisma/adapter-pg` ด้วย — ขาดไม่ได้!
|
||||
|
||||
---
|
||||
|
||||
## 7. วิธีรัน Seed
|
||||
|
||||
```bash
|
||||
# วิธีที่ 1: รันตรงๆ (แนะนำสำหรับ debug)
|
||||
cd apps/backend
|
||||
node prisma/seed.js
|
||||
|
||||
# วิธีที่ 2: ผ่าน Prisma CLI (ต้องตั้งค่าใน prisma.config.ts แล้ว)
|
||||
cd apps/backend
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Error ที่พบบ่อยและวิธีแก้
|
||||
|
||||
### ❌ Error: `url is no longer supported in schema files`
|
||||
|
||||
```
|
||||
The datasource property `url` is no longer supported in schema files.
|
||||
```
|
||||
|
||||
**แก้:** ลบ `url = env("DATABASE_URL")` ออกจาก `schema.prisma`
|
||||
**ย้ายไปใส่:** `prisma.config.ts` → `datasource.url`
|
||||
|
||||
---
|
||||
|
||||
### ❌ Error: `PrismaClient needs to be constructed with a non-empty, valid PrismaClientOptions`
|
||||
|
||||
```
|
||||
PrismaClientInitializationError: `PrismaClient` needs to be constructed with...
|
||||
```
|
||||
|
||||
**แก้:** ต้องส่ง `adapter` เข้าไปใน constructor เสมอ
|
||||
|
||||
```javascript
|
||||
// ❌ ผิด
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ✅ ถูก
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Error: `Unknown property datasourceUrl provided to PrismaClient constructor`
|
||||
|
||||
```
|
||||
PrismaClientConstructorValidationError: Unknown property datasourceUrl...
|
||||
```
|
||||
|
||||
**แก้:** Prisma 7 ไม่รับ `datasourceUrl` แล้ว ต้องใช้ `adapter` แทน
|
||||
|
||||
```javascript
|
||||
// ❌ ผิด (เคยใช้ได้ใน Prisma 6)
|
||||
const prisma = new PrismaClient({ datasourceUrl: process.env.DATABASE_URL });
|
||||
|
||||
// ✅ ถูก
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Error: `⚠️ No seed command configured`
|
||||
|
||||
```
|
||||
⚠️ No seed command configured
|
||||
```
|
||||
|
||||
**แก้:** เพิ่ม `seed` ใน `prisma.config.ts` (ไม่ใช่แค่ใน `package.json` อีกต่อไป)
|
||||
|
||||
```typescript
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "node prisma/seed.js", // ← ต้องมีบรรทัดนี้
|
||||
},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Checklist ก่อนทำ Seed ทุกครั้ง
|
||||
|
||||
```
|
||||
□ 1. schema.prisma → datasource ไม่มี url
|
||||
□ 2. prisma.config.ts → datasource.url ตั้งค่าไว้
|
||||
□ 3. prisma.config.ts → migrations.seed ชี้ถูก
|
||||
□ 4. .env มี DATABASE_URL ครบถ้วน
|
||||
□ 5. ติดตั้ง @prisma/adapter-pg และ pg แล้ว
|
||||
□ 6. seed.js ใช้ PrismaPg adapter ในการสร้าง PrismaClient
|
||||
□ 7. รัน npx prisma db push หรือ migrate ก่อน seed
|
||||
□ 8. รัน node prisma/seed.js เพื่อ seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. สรุปภาพรวม Flow
|
||||
|
||||
```
|
||||
.env (DATABASE_URL)
|
||||
↓
|
||||
prisma.config.ts (datasource.url)
|
||||
↓ npx prisma db push/migrate
|
||||
schema.prisma (Models) → Database Tables
|
||||
↓ node prisma/seed.js
|
||||
seed.js (PrismaPg adapter + PrismaClient) → Insert data → DB
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# รายละเอียดโปรเจกต์ iRelax Aroma
|
||||
|
||||
โปรเจกต์นี้เป็นระบบ E-commerce สำหรับผลิตภัณฑ์เครื่องหอมและน้ำมันหอมระเหย (Aroma & Essential Oils) โดยมีการออกแบบสถาปัตยกรรมแบบ Monorepo ที่แบ่งส่วนการทำงานออกเป็น 4 ส่วนหลักในโฟลเดอร์ `apps/` ดังนี้:
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend (`apps/backend`)
|
||||
**ศูนย์กลางการประมวลผลและฐานข้อมูล**
|
||||
|
||||
* **เทคโนโลยีที่ใช้:** Bun, Elysia.js, Prisma 7, PostgreSQL
|
||||
* **การทำงาน:** ทำหน้าที่เป็น RESTful API Server ที่จัดการข้อมูลทั้งหมดของระบบ ตั้งแต่การตรวจสอบสิทธิ์ (Authentication), การจัดการสินค้า (Products), หมวดหมู่ (Categories), คำสั่งซื้อ (Orders) ไปจนถึงการตั้งค่าต่างๆ ของร้านค้า
|
||||
* **เป้าหมาย:** เพื่อให้เป็นจุดเชื่อมต่อข้อมูลเดียว (Single Source of Truth) ที่มีความรวดเร็วสูงด้วย Bun และมีความปลอดภัยในการเข้าถึงข้อมูล
|
||||
* **การเชื่อมโยง:** เชื่อมต่อกับฐานข้อมูล PostgreSQL และให้บริการ API แก่ `admin`, `ssr`, และ `website`
|
||||
|
||||
## 2. Admin Portal (`apps/admin`)
|
||||
**ระบบจัดการหลังบ้าน (Back-office)**
|
||||
|
||||
* **เทคโนโลยีที่ใช้:** React, Vite, Tailwind CSS
|
||||
* **การทำงาน:** เป็น Web Application สำหรับผู้ดูแลระบบ (Admin) เพื่อใช้ในการจัดการสต็อกสินค้า, ดูรายงานยอดขาย, จัดการบทความ (Articles), และควบคุมการตั้งค่าหน้าร้าน
|
||||
* **เป้าหมาย:** เพื่อให้ผู้ดูแลระบบสามารถบริหารจัดการร้านค้าได้อย่างมีประสิทธิภาพผ่าน Interface ที่ใช้งานง่าย
|
||||
* **การเชื่อมโยง:** เชื่อมต่อกับ `backend` ผ่าน API เพื่อดึงและอัปเดตข้อมูลต่างๆ
|
||||
|
||||
## 3. Main Website (`apps/ssr`)
|
||||
**เว็บไซต์หลัก (Public Interface)**
|
||||
|
||||
* **เทคโนโลยีที่ใช้:** Next.js (React), Tailwind CSS
|
||||
* **การทำงาน:** เป็นส่วนการแสดงผลที่ใช้ Server-Side Rendering (SSR) เพื่อให้รองรับการทำ SEO (Search Engine Optimization) ได้ดีที่สุด โดยจะเน้นการแสดงสินค้าและบทความให้แก่บุคคลทั่วไป
|
||||
* **เป้าหมาย:** เพื่อสร้างการรับรู้แบรนด์ (Brand Awareness) และช่วยให้ลูกค้าค้นหาพบผ่าน Search Engine ได้ง่าย
|
||||
* **การเชื่อมโยง:** เชื่อมต่อกับ `backend` เพื่อดึงข้อมูลสินค้าและเนื้อหามาแสดงผลบนหน้าเว็บ
|
||||
|
||||
## 4. LIFF Mobile App (`apps/website`)
|
||||
**หน้ากากมือถือสำหรับลูกค้า (Customer Interface)**
|
||||
|
||||
* **เทคโนโลยีที่ใช้:** React, Vite, LINE Front-end Framework (LIFF), Tailwind CSS
|
||||
* **การทำงาน:** เป็น Single Page Application (SPA) ที่ออกแบบมาเพื่อรันภายในแอปพลิเคชัน LINE โดยเฉพาะ ลูกค้าสามารถสั่งซื้อสินค้า, ติดตามสถานะคำสั่งซื้อ, และรับสิทธิพิเศษต่างๆ ได้ทันทีผ่าน LINE OA
|
||||
* **เป้าหมาย:** เพื่อความสะดวกสบายของลูกค้าในการเข้าถึงร้านค้าผ่านช่องทางที่คุ้นเคย (LINE) และสร้างความผูกพันกับลูกค้า (Customer Loyalty)
|
||||
* **การเชื่อมโยง:** เชื่อมต่อกับ LINE Platform ผ่าน LIFF API และเชื่อมต่อกับ `backend` เพื่อทำรายการสั่งซื้อและจัดการข้อมูลสมาชิก
|
||||
|
||||
---
|
||||
|
||||
## สรุปการเชื่อมโยงของระบบ
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "External Platforms"
|
||||
PostgreSQL[(Database)]
|
||||
LINE[LINE Platform]
|
||||
end
|
||||
|
||||
subgraph "Internal Apps"
|
||||
B[Backend - ElysiaJS]
|
||||
A[Admin - React/Vite]
|
||||
S[Main Website - Next.js]
|
||||
W[LIFF App - React/Vite]
|
||||
end
|
||||
|
||||
A -- API Calls --> B
|
||||
S -- API Calls --> B
|
||||
W -- API Calls --> B
|
||||
W -- Integration --> LINE
|
||||
B -- Query/Save --> PostgreSQL
|
||||
```
|
||||
|
||||
* **Backend** คือหัวใจหลักที่ทุกส่วนต้องวิ่งมาหา
|
||||
* **Admin** จัดการข้อมูลทั้งหมดผ่าน Backend
|
||||
* **Main Website (SSR)** ดึงข้อมูลไปแสดงเพื่อผลทางด้าน Google Search/SEO
|
||||
* **LIFF App** เป็นช่องทางหลักที่ลูกค้าใช้สื่อสารและสั่งซื้อสินค้าผ่านแอป LINE
|
||||
|
||||
---
|
||||
|
||||
## 5. การ Deployment และ CI/CD
|
||||
|
||||
โปรเจกต์นี้ใช้ระบบ **Git Hooks (post-receive)** ในการทำ Automated Deployment เมื่อมีการ Push โค้ดไปยังเซิร์ฟเวอร์
|
||||
|
||||
### Git Remote Configuration
|
||||
คุณสามารถส่งโค้ดขึ้นเซิร์ฟเวอร์เพื่อ Deploy ได้ผ่านคำสั่ง:
|
||||
```bash
|
||||
git push server master
|
||||
```
|
||||
* **Remote Name:** `server`
|
||||
* **Remote URL:** `ti@kitizone.com:/home/ti/git/irelax.git`
|
||||
|
||||
### ขั้นตอนการทำงานของสคริปต์ Deployment (`post-receive`)
|
||||
เมื่อมีการ Push โค้ด สคริปต์จะทำงานโดยอัตโนมัติดังนี้:
|
||||
|
||||
1. **Checkout & Build**: ดึงโค้ดล่าสุดมายังโฟลเดอร์ชั่วคราวและรัน `bun install`
|
||||
2. **Frontend Deployment**:
|
||||
* Build `website` และ `admin` (Vite)
|
||||
* Deploy ไฟล์ `dist` ไปยัง Nginx paths (`/home/ti/projects/MyNginxMain/nginx/html/irelaxaroma.shop/`)
|
||||
3. **Backend Deployment**:
|
||||
* Sync โค้ดไปยัง `/home/ti/projects/irelax-api`
|
||||
* รัน `prisma generate` และ `prisma migrate deploy` เพื่ออัปเดตฐานข้อมูล
|
||||
* Restart บริการด้วย **PM2** (ชื่อ process: `irelax-backend`)
|
||||
4. **SSR Deployment**:
|
||||
* Build Next.js app และ Sync ไปยัง `/home/ti/projects/irelax-ssr`
|
||||
* Restart บริการด้วย **PM2** (ชื่อ process: `irelax-ssr`) บนพอร์ต `3001`
|
||||
5. **Cleanup**: ลบไฟล์ชั่วคราวที่ใช้ในการ Build ออก
|
||||
|
||||
### สรุป URL การเข้าใช้งาน (Production)
|
||||
* **Website (SPA):** [https://irelaxaroma.shop](https://irelaxaroma.shop)
|
||||
* **Admin Panel:** [https://admin.irelaxaroma.shop](https://admin.irelaxaroma.shop)
|
||||
* **API Backend:** [https://api.irelaxaroma.shop](https://api.irelaxaroma.shop)
|
||||
* **SSR Next.js:** [https://ssr.irelaxaroma.shop](https://ssr.irelaxaroma.shop)
|
||||
|
||||
---
|
||||
|
||||
## 6. ข้อควรปฏิบัติในการอัปเดตฐานข้อมูล (Prisma)
|
||||
|
||||
เพื่อให้ระบบ Deployment อัปเดตฐานข้อมูลบนเซิร์ฟเวอร์ได้อย่างถูกต้องและปลอดภัย ควรปฏิบัติตามขั้นตอนนี้:
|
||||
|
||||
1. **ที่เครื่องคอมพิวเตอร์ของคุณ (Local):**
|
||||
* ทำการแก้ไขไฟล์ `apps/backend/prisma/schema.prisma` ตามต้องการ
|
||||
* รันคำสั่งเพื่อสร้างไฟล์ Migration ใหม่:
|
||||
```bash
|
||||
npx prisma migrate dev --name <ระบุชื่อการเปลี่ยนแปลง>
|
||||
```
|
||||
* Prisma จะสร้างไฟล์ในโฟลเดอร์ `apps/backend/prisma/migrations/`
|
||||
2. **การ Commit โค้ด:**
|
||||
* ต้องทำการ Commit ทั้งไฟล์ `schema.prisma` และ **โฟลเดอร์ migrations** ที่เกิดขึ้นใหม่เข้าไปใน Git ด้วยทุกครั้ง
|
||||
3. **การ Deploy:**
|
||||
* เมื่อสั่ง `git push server master` ระบบ Git Hook บนเซิร์ฟเวอร์จะตรวจพบไฟล์ Migration ใหม่
|
||||
* สคริปต์จะรัน `prisma migrate deploy` ให้โดยอัตโนมัติ เพื่อนำความเปลี่ยนแปลงเข้าสู่ฐานข้อมูล Production โดยไม่ทำให้ข้อมูลเดิมสูญหาย
|
||||
@@ -0,0 +1,89 @@
|
||||
ตะไคร้,120mlSeller SKU: 120-ตะไคร้-120ml
|
||||
ลาเวนเดอร์,120mlSeller SKU: 120-ลาเวนเดอร์-120ml
|
||||
ลาเวนเดอร์,30mlSeller SKU: 30-ลาเวนเดอร์-30ml
|
||||
วานิลลา,120mlSeller SKU: 120-วานิลลา-120ml
|
||||
วานิลลา,30mlSeller SKU: 30-วานิลลา-30ml
|
||||
ซากุระ,120mlSeller SKU: 120-ซากุระ-120ml
|
||||
ซากุระ,30mlSeller SKU: 30-ซากุระ-30ml
|
||||
มะลิ,120mlSeller SKU: 120-มะลิ-120ml
|
||||
มะลิ,30mlSeller SKU: 30-มะลิ-30ml
|
||||
ยูคาลิปตัส,120mlSeller SKU: 120-ยูคาลิปตัส-120ml
|
||||
ยูคาลิปตัส,30mlSeller SKU: 30-ยูคาลิปตัส-30ml
|
||||
เฟรชลาเวนเดอร์,120mlSeller SKU: 120-เฟรชลาเวนเดอร์-120ml
|
||||
เฟรชลาเวนเดอร์,30mlSeller SKU: 30-เฟรชลาเวนเดอร์-30ml
|
||||
เปเปอร์มิ้น,120mlSeller SKU: 120-เปเปอร์มิ้น-120ml
|
||||
เปเปอร์มิ้น,30mlSeller SKU: 30-เปเปอร์มิ้น-30ml
|
||||
โอเชี่ยน,120mlSeller SKU: 120-โอเชี่ยน-120ml
|
||||
โอเชี่ยน,30mlSeller SKU: 30-โอเชี่ยน-30ml
|
||||
เซเว่นซี,120mlSeller SKU: 120-เซเว่นซี-120ml
|
||||
เซเว่นซี,30mlSeller SKU: 30-เซเว่นซี-30ml
|
||||
ดอกแก้ว,120mlSeller SKU: 120-ดอกแก้ว-120ml
|
||||
ดอกแก้ว,30mlSeller SKU: 30-ดอกแก้ว-30ml
|
||||
ดอกโมก,120mlSeller SKU: 120-ดอกโมก-120ml
|
||||
ดอกโมก,30mlSeller SKU: 30-ดอกโมก-30ml
|
||||
ลีลาวดี,120mlSeller SKU: 120-ลีลาวดี-120ml
|
||||
ลีลาวดี,30mlSeller SKU: 30-ลีลาวดี-30ml
|
||||
ส้ม,120mlSeller SKU: 120-ส้ม-120ml
|
||||
ส้ม,30mlSeller SKU: 30-ส้ม-30ml
|
||||
มะนาว,120mlSeller SKU: 120-มะนาว-120ml
|
||||
มะนาว,30mlSeller SKU: 30-มะนาว-30ml
|
||||
แคนตาลูป,120mlSeller SKU: 120-แคนตาลูป-120ml
|
||||
แคนตาลูป,30mlSeller SKU: 30-แคนตาลูป-30ml
|
||||
แอปเปิ้ลแดง,120mlSeller SKU: 120-แอปเปิ้ลแดง-120ml
|
||||
แอปเปิ้ลแดง,30mlSeller SKU: 30-แอปเปิ้ลแดง-30ml
|
||||
แอปเปิ้ลเขียว,120mlSeller SKU: 120-แอปเปิ้ลเขียว-120ml
|
||||
แอปเปิ้ลเขียว,30mlSeller SKU: 30-แอปเปิ้ลเขียว-30ml
|
||||
ดอกลิลลี่,120mlSeller SKU: 120-ดอกลิลลี่-120ml
|
||||
ดอกลิลลี่,30mlSeller SKU: 30-ดอกลิลลี่-30ml
|
||||
โรสเมรี่,120mlSeller SKU: 120-โรสเมรี่-120ml
|
||||
โรสเมรี่,30mlSeller SKU: 30-โรสเมรี่-30ml
|
||||
อัญชัน,120mlSeller SKU: 120-อัญชัน-120ml
|
||||
อัญชัน,30mlSeller SKU: 30-อัญชัน-30ml
|
||||
กุหลาบแดง,120mlSeller SKU: 120-กุหลาบแดง-120ml
|
||||
กุหลาบแดง,30mlSeller SKU: 30-กุหลาบแดง-30ml
|
||||
หมากฝรั่ง,120mlSeller SKU: 120-หมากฝรั่ง-120ml
|
||||
หมากฝรั่ง,30mlSeller SKU: 30-หมากฝรั่ง-30ml
|
||||
ผลไม้รวม,120mlSeller SKU: 120-ผลไม้รวม-120ml
|
||||
ผลไม้รวม,30mlSeller SKU: 30-ผลไม้รวม-30ml
|
||||
มะกรูด,120mlSeller SKU: 120-มะกรูด-120ml
|
||||
มะกรูด,30mlSeller SKU: 30-มะกรูด-30ml
|
||||
แซนด้าวูส,120mlSeller SKU: 120-แซนด้าวูส-120ml
|
||||
แซนด้าวูส,30mlSeller SKU: 30-แซนด้าวูส-30ml
|
||||
ดอกปีบ,120mlSeller SKU: 120-ดอกปีบ-120ml
|
||||
ดอกปีบ,30mlSeller SKU: 30-ดอกปีบ-30ml
|
||||
ดอกบัว,120mlSeller SKU: 120-ดอกบัว-120ml
|
||||
ดอกบัว,30mlSeller SKU: 30-ดอกบัว-30ml
|
||||
โอโซน,120mlSeller SKU: 120-โอโซน-120ml
|
||||
โอโซน,30mlSeller SKU: 30-โอโซน-30ml
|
||||
ไทยสปา,120mlSeller SKU: 120-ไทยสปา-120ml
|
||||
ไทยสปา,30mlSeller SKU: 30-ไทยสปา-30ml
|
||||
กล้วย,120mlSeller SKU: 120-กล้วย-120ml
|
||||
กล้วย,30mlSeller SKU: 30-กล้วย-30ml
|
||||
กาแฟ,120mlSeller SKU: 120-กาแฟ-120ml
|
||||
กาแฟ,30mlSeller SKU: 30-กาแฟ-30ml
|
||||
ดอกชมนาด,120mlSeller SKU: 120-ดอกชมนาด-120ml
|
||||
ดอกชมนาด,30mlSeller SKU: 30-ดอกชมนาด-30ml
|
||||
กลิ่นน้ำนม,120mlSeller SKU: 120-กลิ่นน้ำนม-120ml
|
||||
กลิ่นน้ำนม,30mlSeller SKU: 30-กลิ่นน้ำนม-30ml
|
||||
ดอกการะเวก,120mlSeller SKU: 120-ดอกการะเวก-120ml
|
||||
ดอกการะเวก,30mlSeller SKU: 30-ดอกการะเวก-30ml
|
||||
ดอกจำปา,120mlSeller SKU: 120-ดอกจำปา-120ml
|
||||
ดอกจำปา,30mlSeller SKU: 30-ดอกจำปา-30ml
|
||||
ดอกพิกุล,120mlSeller SKU: 120-ดอกพิกุล-120ml
|
||||
ดอกพิกุล,30mlSeller SKU: 30-ดอกพิกุล-30ml
|
||||
ดอกพุด,120mlSeller SKU: 120-ดอกพุด-120ml
|
||||
ดอกพุด,30mlSeller SKU: 30-ดอกพุด-30ml
|
||||
ดอกราตรี,120mlSeller SKU: 120-ดอกราตรี-120ml
|
||||
ดอกราตรี,30mlSeller SKU: 30-ดอกราตรี-30ml
|
||||
ดอกลำดวน,120mlSeller SKU: 120-ดอกลำดวน-120ml
|
||||
ดอกลำดวน,30mlSeller SKU: 30-ดอกลำดวน-30ml
|
||||
ทับทิม,120mlSeller SKU: 120-ทับทิม-120ml
|
||||
ทับทิม,30mlSeller SKU: 30-ทับทิม-30ml
|
||||
มะละกอสุก,120mlSeller SKU: 120-มะละกอสุก-120ml
|
||||
มะละกอสุก,30mlSeller SKU: 30-มะละกอสุก-30ml
|
||||
ดอกราชาวดี,120mlSeller SKU: 120-ดอกราชาวดี-120ml
|
||||
ดอกราชาวดี,30mlSeller SKU: 30-ดอกราชาวดี-30ml
|
||||
กลิ่นลุกพีช,120mlSeller SKU: 120-กลิ่นลุกพีช-120ml
|
||||
กลิ่นลุกพีช,30mlSeller SKU: 30-กลิ่นลุกพีช-30ml
|
||||
กลิ่นองุ่น,120mlSeller SKU: 120-กลิ่นองุ่น-120ml
|
||||
กลิ่นองุ่น,30ml
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_URL="https://api.homaroma.shop/api"
|
||||
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
!.env.production
|
||||
@@ -0,0 +1,73 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@homaroma/types": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-quill-new": "^3.8.3",
|
||||
"react-router-dom": "^7.14.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filtLine truncated
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,184 @@
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import AdminLayout from './components/AdminLayout'
|
||||
import DashboardPage from './pages/DashboardPage'
|
||||
import ProductsPage from './pages/ProductsPage'
|
||||
import ProductEditPage from './pages/ProductEditPage'
|
||||
import CategoriesPage from './pages/CategoriesPage'
|
||||
import CustomersPage from './pages/CustomersPage'
|
||||
import HeroSettingsPage from './pages/HeroSettingsPage'
|
||||
import RawMaterialsPage from './pages/RawMaterialsPage'
|
||||
import LoginPage from './pages/LoginPage'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
import OrdersPage from './pages/OrdersPage'
|
||||
import OrderDetailPage from './pages/OrderDetailPage'
|
||||
import SystemSettingsPage from './pages/SystemSettingsPage'
|
||||
import ArticlesPage from './pages/ArticlesPage'
|
||||
import ArticleEditPage from './pages/ArticleEditPage'
|
||||
import CouponsPage from './pages/CouponsPage'
|
||||
import TiktokPage from './pages/TiktokPage'
|
||||
import TiktokAnalysisPage from './pages/TiktokAnalysisPage'
|
||||
import ShopeePage from './pages/ShopeePage'
|
||||
import ShopeeOrderDetailPage from './pages/ShopeeOrderDetailPage'
|
||||
import ShopeeAnalyticsPage from './pages/ShopeeAnalyticsPage'
|
||||
import ShopeeOverviewPage from './pages/ShopeeOverviewPage'
|
||||
import TiktokOrderPage from './pages/TiktokOrderPage'
|
||||
import TiktokAnalyticsPage from './pages/TiktokAnalyticsPage'
|
||||
import TiktokOverviewPage from './pages/TiktokOverviewPage'
|
||||
import LazadaOrderPage from './pages/LazadaOrderPage'
|
||||
import LazadaAnalyticsPage from './pages/LazadaAnalyticsPage'
|
||||
import LazadaOverviewPage from './pages/LazadaOverviewPage'
|
||||
import GlobalAnalyticsPage from './pages/GlobalAnalyticsPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public Route */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
{/* Guarded Admin Routes */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route element={<AdminLayout />}>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/products" element={<ProductsPage />} />
|
||||
<Route path="/products/:id/edit" element={<ProductEditPage />} />
|
||||
<Route path="/categories" element={<CategoriesPage />} />
|
||||
<Route path="/customers" element={<CustomersPage />} />
|
||||
<Route path="/hero" element={<HeroSettingsPage />} />
|
||||
<Route path="/orders" element={<OrdersPage />} />
|
||||
<Route path="/orders/:id" element={<OrderDetailPage />} />
|
||||
<Route path="/coupons" element={<CouponsPage />} />
|
||||
<Route path="/raw-materials" element={<RawMaterialsPage />} />
|
||||
<Route path="/settings" element={<SystemSettingsPage />} />
|
||||
<Route path="/articles" element={<ArticlesPage />} />
|
||||
<Route path="/articles/:id" element={<ArticleEditPage />} />
|
||||
<Route path="/tiktok" element={<TiktokPage />} />
|
||||
<Route path="/tiktok-analysis" element={<TiktokAnalysisPage />} />
|
||||
<Route path="/shopee" element={<ShopeePage />} />
|
||||
<Route path="/shopee/:orderId" element={<ShopeeOrderDetailPage />} />
|
||||
<Route path="/shopee-analytics" element={<ShopeeAnalyticsPage />} />
|
||||
<Route path="/shopee-overview" element={<ShopeeOverviewPage />} />
|
||||
<Route path="/tiktok-order" element={<TiktokOrderPage />} />
|
||||
<Route path="/tiktok-analytics" element={<TiktokAnalyticsPage />} />
|
||||
<Route path="/tiktok-overview" element={<TiktokOverviewPage />} />
|
||||
<Route path="/lazada-order" element={<LazadaOrderPage />} />
|
||||
<Route path="/lazada-analytics" element={<LazadaAnalyticsPage />} />
|
||||
<Route path="/lazada-overview" element={<LazadaOverviewPage />} />
|
||||
<Route path="/global-analytics" element={<GlobalAnalyticsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="usLine truncated
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,15 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
export default function AdminLayout() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-night-950">
|
||||
<Sidebar />
|
||||
<main className="flex-1 ml-72 p-10">
|
||||
<div className="w-full max-w-[1600px] mx-auto space-y-10">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Navigate, Outlet } from 'react-router-dom'
|
||||
|
||||
export default function ProtectedRoute() {
|
||||
const token = localStorage.getItem('irelax_admin_token')
|
||||
|
||||
if (!token) {
|
||||
// If no token, redirect to login page
|
||||
return <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
// If token exists, render the child routes (Outlet)
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
PackageOpen,
|
||||
Settings,
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
ShoppingCart,
|
||||
ShoppingBag,
|
||||
BookOpen,
|
||||
TrendingUp,
|
||||
Image,
|
||||
Tags,
|
||||
Ticket,
|
||||
BarChart3,
|
||||
Layers,
|
||||
Globe2,
|
||||
Users
|
||||
} from 'lucide-react'
|
||||
|
||||
const MENU_ITEMS = [
|
||||
{ icon: LayoutDashboard, label: 'Dashboard', to: '/' },
|
||||
{ icon: Tags, label: 'Categories', to: '/categories' },
|
||||
{ icon: Package, label: 'Products', to: '/products' },
|
||||
{ icon: PackageOpen, label: 'Raw Materials', to: '/raw-materials' },
|
||||
{ icon: Users, label: 'LINE Customers', to: '/customers' },
|
||||
{ icon: ShoppingCart, label: 'Orders', to: '/orders' },
|
||||
{ icon: Ticket, label: 'Coupons', to: '/coupons' },
|
||||
{ icon: Image, label: 'Hero Section', to: '/hero' },
|
||||
{ icon: BookOpen, label: 'Scent Guide', to: '/articles' },
|
||||
{ icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' },
|
||||
{ icon: TrendingUp, label: 'TikTok Analysis', to: '/tiktok-analysis' },
|
||||
{ icon: ShoppingBag, label: 'Shopee (Hom Aroma)', to: '/shopee' },
|
||||
{ icon: BarChart3, label: 'Shopee Analytics', to: '/shopee-analytics' },
|
||||
{ icon: Layers, label: 'Shopee ทุกร้าน', to: '/shopee-overview' },
|
||||
{ icon: ShoppingCart, label: 'TikTok Orders', to: '/tiktok-order' },
|
||||
{ icon: BarChart3, label: 'TikTok Analytics', to: '/tiktok-analytics' },
|
||||
{ icon: Layers, label: 'TikTok ทุกร้าน', to: '/tiktok-overview' },
|
||||
{ icon: ShoppingCart, label: 'Lazada Orders', to: '/lazada-order' },
|
||||
{ icon: BarChart3, label: 'Lazada Analytics', to: '/lazada-analytics' },
|
||||
{ icon: Layers, label: 'Lazada ทุกร้าน', to: '/lazada-overview' },
|
||||
{ icon: Globe2, label: 'All Analytics', to: '/global-analytics' },
|
||||
{ icon: Settings, label: 'Settings', to: '/settings' },
|
||||
]
|
||||
|
||||
export default function Sidebar() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('irelax_admin_token')
|
||||
localStorage.removeItem('irelax_admin_user')
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-72 h-screen fixed left-0 top-0 bg-night-900 border-r border-white/5 flex flex-col z-50">
|
||||
{/* Brand */}
|
||||
<div className="p-8">
|
||||
<div className="text-xl font-bold tracking-widest text-white uppercase italic">
|
||||
iRelax <span className="text-cyan-glow text-xs not-italic font-light opacity-60">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation — scrollable */}
|
||||
<nav className="flex-1 min-h-0 overflow-y-auto px-4 py-2 space-y-1 sidebar-nav">
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
className={({ isActive }) =>
|
||||
`sidebar-link ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
|
||||
}
|
||||
>
|
||||
<item.icon className="w-5 h-5 shrink-0" />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
<ChevronRight className="w-4 h-4 opacity-0 group-hover:opacity-40 transition-opacity" />
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User / Bottom */}
|
||||
<div className="p-4 border-t border-white/5 space-y-2">
|
||||
<div className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-all group">
|
||||
<div className="w-9 h-9 rounded-full bg-cyan-glow/20 flex items-center justify-center text-cyan-glow font-bold text-xs border border-cyan-glow/30">
|
||||
A
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="text-sm font-bold text-white truncate">Admin Account</p>
|
||||
<p className="text-[10px] uppercase tracking-wider text-slate-500">System Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-rose-500 hover:bg-rose-500/10 transition-all cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* iRelax Admin - Professional Dark Palette */
|
||||
--color-night-950: #050608;
|
||||
--color-night-900: #0B0C10;
|
||||
--color-night-800: #1F2833;
|
||||
--color-night-700: #45A29E;
|
||||
--color-cyan-glow: #66FCF1;
|
||||
--color-silver-metallic: #C5C6C7;
|
||||
--color-slate-dark: #1e293b;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-night-950 text-silver-metallic font-sans antialiased;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
@apply text-white tracking-tight;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.admin-card {
|
||||
@apply bg-night-900/50 backdrop-blur-md border border-white/5 rounded-xl p-6 transition-all duration-300;
|
||||
}
|
||||
|
||||
.admin-card-hover {
|
||||
@apply hover:border-cyan-glow/20 hover:shadow-[0_0_20px_rgba(102,252,241,0.05)];
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-cyan-glow text-night-900 px-4 py-2 rounded-lg font-bold text-sm tracking-wide uppercase transition-all duration-300 hover:bg-white hover:shadow-[0_0_15px_rgba(102,252,241,0.3)];
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply px-4 py-2 rounded-lg text-sm text-slate-400 hover:bg-white/5 hover:text-white transition-all;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
@apply flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200;
|
||||
}
|
||||
|
||||
.sidebar-link-active {
|
||||
@apply bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20 shadow-[0_0_15px_rgba(102,252,241,0.05)];
|
||||
}
|
||||
|
||||
.sidebar-link-inactive {
|
||||
@apply text-slate-400 hover:bg-white/5 hover:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Admin */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-night-950;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-night-800 rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-night-700;
|
||||
}
|
||||
|
||||
/* Sidebar Nav — custom thin scrollbar */
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||||
@apply bg-night-800 rounded-full;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-cyan-glow/40;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
.sidebar-nav {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #1F2833 transparent;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-zoom-in {
|
||||
animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,241 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import { API_BASE } from '@irelax/types'
|
||||
import { ArrowLeft, Save, Loader2, Image as ImageIcon } from 'lucide-react'
|
||||
import ReactQuill from 'react-quill-new'
|
||||
import 'react-quill-new/dist/quill.snow.css'
|
||||
|
||||
export default function ArticleEditPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isNew = id === 'new'
|
||||
const navigate = useNavigate()
|
||||
const quillRef = useRef<any>(null)
|
||||
|
||||
const [loading, setLoading] = useState(!isNew)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
const [title, setTitle] = useState('')
|
||||
const [slug, setSlug] = useState('')
|
||||
const [coverImage, setCoverImage] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
const [isPublished, setIsPublished] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isNew) return
|
||||
fetch(`${API_BASE}/articles/id/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setTitle(data.title)
|
||||
setSlug(data.slug)
|
||||
setCoverImage(data.coverImage || '')
|
||||
setContent(data.content)
|
||||
setIsPublished(data.isPublished)
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [id, isNew])
|
||||
|
||||
// Automatically generate slug from title if it's new
|
||||
useEffect(() => {
|
||||
if (isNew && title && !slug.includes('-')) {
|
||||
setSlug(title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, ''))
|
||||
}
|
||||
}, [title, isNew])
|
||||
|
||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
setUploading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/upload`, { method: 'POST', body: formData })
|
||||
const data = await res.json()
|
||||
if (data.url) setCoverImage(data.url)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Upload failed')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = { title, slug, coverImage, content, isPublished }
|
||||
const res = await fetch(`${API_BASE}/articles${isNew ? '' : `/${id}`}`, {
|
||||
method: isNew ? 'POST' : 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
navigate('/articles')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to save article. Make sure the slug is unique.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const imageHandler = () => {
|
||||
const url = prompt('Please enter the Image URL: \n(Or leave blank and click OK to upload from your computer)')
|
||||
|
||||
if (url) {
|
||||
const quill = quillRef.current?.getEditor()
|
||||
if (quill) {
|
||||
const range = quill.getSelection(true)
|
||||
quill.insertEmbed(range?.index || 0, 'image', url)
|
||||
}
|
||||
} else if (url === "") {
|
||||
const input = document.createElement('input')
|
||||
input.setAttribute('type', 'file')
|
||||
input.setAttribute('accept', 'image/*')
|
||||
input.click()
|
||||
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const quill = quillRef.current?.getEditor()
|
||||
if (quill) {
|
||||
const range = quill.getSelection(true)
|
||||
quill.insertEmbed(range?.index || 0, 'image', e.target?.result)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modules = useMemo(() => ({
|
||||
toolbar: {
|
||||
container: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike', 'blockquote'],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
],
|
||||
handlers: {
|
||||
image: imageHandler
|
||||
}
|
||||
}
|
||||
}), [])
|
||||
|
||||
if (loading) return <div className="p-8 text-center"><Loader2 className="w-8 h-8 animate-spin mx-auto text-cyan-glow" /></div>
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-8 animate-fade-in relative pb-20">
|
||||
<div className="flex items-center gap-4 border-b border-white/5 pb-6">
|
||||
<Link to="/articles" className="p-2 glass rounded-full hover:bg-white/10 transition-colors">
|
||||
<ArrowLeft className="w-5 h-5 text-slate-400" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{isNew ? 'New Article' : 'Edit Article'}</h1>
|
||||
<p className="text-slate-500 text-sm">Write engaging guides for your audience.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-6">
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Post Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
className="admin-input w-full text-lg font-bold"
|
||||
placeholder="e.g. 5 Benefits of Lavender Oil"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">URL Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={slug}
|
||||
onChange={e => setSlug(e.target.value)}
|
||||
className="admin-input w-full font-mono text-sm text-cyan-glow"
|
||||
placeholder="e.g. benefits-of-lavender"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Cover Image</label>
|
||||
<div className="relative border-2 border-dashed border-white/10 rounded-xl bg-night-950 hover:border-cyan-glow/50 transition-colors group cursor-pointer overflow-hidden aspect-[4/3] flex flex-col items-center justify-center text-center">
|
||||
{coverImage ? (
|
||||
<img src={coverImage} className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity" alt="Cover" />
|
||||
) : (
|
||||
<div className="space-y-2 p-4">
|
||||
{uploading ? <Loader2 className="w-8 h-8 animate-spin mx-auto text-cyan-glow" /> : <ImageIcon className="w-8 h-8 text-slate-500 mx-auto" />}
|
||||
<p className="text-[10px] text-slate-500 uppercase tracking-widest">Upload Cover</p>
|
||||
</div>
|
||||
)}
|
||||
<input type="file" accept="image/*" onChange={handleImageUpload} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Content Body</label>
|
||||
<div className="bg-white text-black rounded-lg overflow-hidden prose-editor-container">
|
||||
<ReactQuill
|
||||
ref={quillRef}
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
modules={modules}
|
||||
className="min-h-[400px]"
|
||||
/>
|
||||
</div>
|
||||
<style>{`
|
||||
.ql-editor { min-height: 400px; font-size: 16px; }
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
<div className="fixed bottom-0 right-0 left-64 p-4 glass border-t border-white/5 flex justify-end gap-4 z-10 hidden md:flex">
|
||||
<label className="flex items-center gap-3 mr-auto cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isPublished}
|
||||
onChange={e => setIsPublished(e.target.checked)}
|
||||
className="w-5 h-5 rounded border-white/20 bg-night-950 text-cyan-glow focus:ring-cyan-glow"
|
||||
/>
|
||||
<span className="text-sm font-bold uppercase tracking-widest">Publish to Website</span>
|
||||
</label>
|
||||
<Link to="/articles" className="btn-secondary px-8 py-3 font-bold text-sm">Cancel</Link>
|
||||
<button type="submit" disabled={saving} className="btn-primary px-10 py-3 flex items-center gap-2 text-sm font-bold">
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Save Article
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions */}
|
||||
<div className="md:hidden space-y-4 pt-8">
|
||||
<label className="flex items-center gap-3 p-4 bg-white/5 border border-white/10 rounded-xl">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isPublished}
|
||||
onChange={e => setIsPublished(e.target.checked)}
|
||||
className="w-5 h-5 rounded"
|
||||
/>
|
||||
<span className="text-sm font-bold uppercase">Publish to Website</span>
|
||||
</label>
|
||||
<button type="submit" disabled={saving} className="btn-primary w-full py-4 text-center justify-center flex items-center gap-2">
|
||||
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Save className="w-5 h-5" />}
|
||||
Save Article
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { API_BASE, type Article } from '@irelax/types'
|
||||
import { Plus, Edit, Trash2, Eye, EyeOff } from 'lucide-react'
|
||||
|
||||
export default function ArticlesPage() {
|
||||
const [articles, setArticles] = useState<Article[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetchArticles = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/articles`)
|
||||
const data = await res.json()
|
||||
setArticles(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchArticles()
|
||||
}, [])
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this article?')) return
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/articles/${id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setArticles(prev => prev.filter(a => a.id !== id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to delete')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Articles / Scent Guide</h1>
|
||||
<p className="text-slate-500 text-sm">Manage educational blog posts and guides.</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/articles/new"
|
||||
className="btn-primary flex items-center gap-2 w-max"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Create Article
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="admin-card overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-[10px] uppercase tracking-widest text-slate-500 bg-black/20">
|
||||
<th className="p-4 rounded-tl-xl w-16">Image</th>
|
||||
<th className="p-4">Title & Slug</th>
|
||||
<th className="p-4">Status</th>
|
||||
<th className="p-4">Date</th>
|
||||
<th className="p-4 rounded-tr-xl text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<tr key={i} className="animate-pulse">
|
||||
<td className="p-4"><div className="w-12 h-12 bg-white/5 rounded-lg" /></td>
|
||||
<td className="p-4"><div className="h-4 w-48 bg-white/5 rounded mb-2" /><div className="h-3 w-32 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-4 w-16 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-4 w-24 bg-white/5 rounded" /></td>
|
||||
<td className="p-4 text-right"><div className="h-8 w-16 bg-white/5 rounded-lg ml-auto" /></td>
|
||||
</tr>
|
||||
))
|
||||
) : articles.length === 0 ? (
|
||||
<tr><td colSpan={5} className="p-8 text-center text-slate-500 italic">No articles found. Create your first guide!</td></tr>
|
||||
) : (
|
||||
articles.map(article => (
|
||||
<tr key={article.id} className="hover:bg-white/5 transition-colors group">
|
||||
<td className="p-4">
|
||||
{article.coverImage ? (
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden border border-white/10">
|
||||
<img src={article.coverImage} alt={article.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-12 h-12 bg-white/5 rounded-lg border border-dashed border-white/20 flex items-center justify-center">
|
||||
<Eye className="w-4 h-4 text-slate-600" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<p className="font-bold">{article.title}</p>
|
||||
<p className="text-xs text-cyan-glow font-mono mt-0.5">/{article.slug}</p>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
{article.isPublished ? (
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-emerald-400">
|
||||
<Eye className="w-4 h-4" /> Published
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-amber-500">
|
||||
<EyeOff className="w-4 h-4" /> Draft
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-sm text-slate-400">
|
||||
{new Date(article.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Link
|
||||
to={`/articles/${article.id}`}
|
||||
className="p-2 glass rounded-lg hover:bg-white/10 transition-colors text-cyan-glow"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(article.id)}
|
||||
className="p-2 glass rounded-lg hover:bg-rose-500/20 hover:text-rose-400 transition-colors text-slate-400"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_BASE, getCategoryImage, type Category } from '@irelax/types'
|
||||
import { Edit3, Package, Trash2, Plus, X, Image as ImageIcon, UploadCloud, Loader2, ClipboardPaste, LayoutDashboard } from 'lucide-react'
|
||||
|
||||
export default function CategoriesPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [editingCategory, setEditingCategory] = useState<Partial<Category> | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const r = await fetch(`${API_BASE}/categories`)
|
||||
const data = await r.json()
|
||||
setCategories(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!editingCategory) return
|
||||
|
||||
try {
|
||||
const url = editingCategory.id
|
||||
? `${API_BASE}/categories/${editingCategory.id}`
|
||||
: `${API_BASE}/categories`
|
||||
const method = editingCategory.id ? 'PUT' : 'POST'
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: editingCategory.name,
|
||||
imageUrl: editingCategory.imageUrl || null
|
||||
})
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setIsModalOpen(false)
|
||||
fetchData()
|
||||
} else {
|
||||
const errorData = await res.json()
|
||||
alert(errorData.message || 'Failed to save category')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Save error:', err)
|
||||
alert('An error occurred while saving.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this category?')) return
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/categories/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
} else {
|
||||
const errorData = await res.json()
|
||||
alert(errorData.message || 'Failed to delete category')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
setIsUploading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
setEditingCategory(prev => ({ ...prev, imageUrl: data.url }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteFromClipboard = async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read()
|
||||
for (const item of items) {
|
||||
const imageTypes = item.types.filter(t => t.startsWith('image/'))
|
||||
for (const type of imageTypes) {
|
||||
const blob = await item.getType(type)
|
||||
const file = new File([blob], 'pasted-category-image.png', { type })
|
||||
handleUpload(file)
|
||||
return
|
||||
}
|
||||
}
|
||||
alert('No image found in clipboard')
|
||||
} catch (err) {
|
||||
console.error('Clipboard error:', err)
|
||||
alert('Please allow clipboard access or copy an image first.')
|
||||
}
|
||||
}
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingCategory({
|
||||
name: '',
|
||||
imageUrl: ''
|
||||
})
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const openEditModal = (cat: Category) => {
|
||||
setEditingCategory(cat)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-fade-in">
|
||||
{!isModalOpen ? (
|
||||
<>
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Category Management</h1>
|
||||
<p className="text-slate-500 text-sm">Managing scent classification and product groups.</p>
|
||||
</div>
|
||||
<button onClick={openAddModal} className="btn-primary flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Create Category
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="admin-card h-48 bg-white/5 animate-pulse" />
|
||||
))
|
||||
) : (
|
||||
categories.map((cat) => (
|
||||
<div key={cat.id} className="admin-card admin-card-hover group relative overflow-hidden">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="w-14 h-14 rounded-2xl overflow-hidden glass border border-white/10 p-1">
|
||||
<img
|
||||
src={cat.imageUrl || getCategoryImage(cat.name)}
|
||||
alt={cat.name}
|
||||
className="w-full h-full object-cover grayscale opacity-40 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity relative z-10">
|
||||
<button
|
||||
onClick={() => openEditModal(cat)}
|
||||
className="p-2 rounded-lg bg-night-800 border border-white/5 text-slate-500 hover:text-cyan-glow hover:border-cyan-glow/30 transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(cat.id)}
|
||||
className="p-2 rounded-lg bg-night-800 border border-white/5 text-slate-500 hover:text-rose-500 hover:border-rose-500/30 transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 relative z-10">
|
||||
<h3 className="text-xl font-bold text-white tracking-tight">{cat.name}</h3>
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
<Package className="w-3 h-3 text-cyan-glow/50" />
|
||||
<span>{cat._count?.products || 0} Products</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-cyan-glow/5 blur-3xl rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
editingCategory && (
|
||||
<div className="w-full max-w-3xl mx-auto bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-fade-in">
|
||||
<div className="p-6 border-b border-white/5 flex flex-shrink-0 items-center justify-between bg-white/[0.02]">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<LayoutDashboard className="w-5 h-5 text-cyan-glow" />
|
||||
{editingCategory.id ? 'Edit Category' : 'Create Category'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-500 hover:text-white transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Image Upload Area */}
|
||||
<div className="md:col-span-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Category Image</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePasteFromClipboard}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow text-[10px] font-bold uppercase tracking-wider transition-all border border-cyan-glow/20"
|
||||
>
|
||||
<ClipboardPaste className="w-3.5 h-3.5" />
|
||||
Paste Image
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="relative w-40 h-40 mx-auto rounded-3xl overflow-hidden border-2 border-dashed border-white/5 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
|
||||
onClick={() => document.getElementById('category-upload')?.click()}
|
||||
>
|
||||
{editingCategory.imageUrl ? (
|
||||
<>
|
||||
<img src={editingCategory.imageUrl} className="w-full h-full object-cover" alt="Category" />
|
||||
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
|
||||
<UploadCloud className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center space-y-2 p-4">
|
||||
<ImageIcon className="w-8 h-8 text-slate-700 mx-auto" />
|
||||
<p className="text-[10px] text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 bg-night-950/60 flex items-center justify-center">
|
||||
<Loader2 className="w-6 h-6 text-cyan-glow animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
id="category-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) handleUpload(file)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Category Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingCategory.name || ''}
|
||||
onChange={(e) => setEditingCategory({ ...editingCategory, name: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. Reed Diffuser"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/5 flex flex-col md:flex-row gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isUploading}
|
||||
className="flex-1 btn-primary py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
{editingCategory.id ? 'Save Changes' : 'Create Category'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_BASE } from '@irelax/types'
|
||||
import { Edit3, Ticket, Trash2, Plus, X, Power, PowerOff } from 'lucide-react'
|
||||
|
||||
interface Coupon {
|
||||
id: number
|
||||
code: string
|
||||
type: string
|
||||
value: number
|
||||
minPurchase: number
|
||||
maxDiscount: number | null
|
||||
usageLimit: number | null
|
||||
usedCount: number
|
||||
expiresAt: string | null
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
export default function CouponsPage() {
|
||||
const [coupons, setCoupons] = useState<Coupon[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [editingCoupon, setEditingCoupon] = useState<Partial<Coupon> | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const r = await fetch(`${API_BASE}/coupons`)
|
||||
const data = await r.json()
|
||||
setCoupons(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!editingCoupon) return
|
||||
|
||||
try {
|
||||
const url = editingCoupon.id
|
||||
? `${API_BASE}/coupons/${editingCoupon.id}`
|
||||
: `${API_BASE}/coupons`
|
||||
const method = editingCoupon.id ? 'PATCH' : 'POST'
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
code: editingCoupon.code,
|
||||
type: editingCoupon.type,
|
||||
value: Number(editingCoupon.value),
|
||||
minPurchase: Number(editingCoupon.minPurchase || 0),
|
||||
maxDiscount: editingCoupon.maxDiscount ? Number(editingCoupon.maxDiscount) : null,
|
||||
usageLimit: editingCoupon.usageLimit ? Number(editingCoupon.usageLimit) : null,
|
||||
expiresAt: editingCoupon.expiresAt || null,
|
||||
isActive: editingCoupon.isActive !== undefined ? editingCoupon.isActive : true
|
||||
})
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setIsModalOpen(false)
|
||||
fetchData()
|
||||
} else {
|
||||
const errorData = await res.json()
|
||||
alert(errorData.error || 'Failed to save coupon')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Save error:', err)
|
||||
alert('An error occurred while saving.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this coupon?')) return
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/coupons/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
} else {
|
||||
const errorData = await res.json()
|
||||
alert(errorData.error || 'Failed to delete coupon')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleActive = async (id: number, currentStatus: boolean) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/coupons/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isActive: !currentStatus })
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Toggle error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingCoupon({
|
||||
code: '',
|
||||
type: 'PERCENTAGE',
|
||||
value: 0,
|
||||
minPurchase: 0,
|
||||
maxDiscount: null,
|
||||
usageLimit: null,
|
||||
expiresAt: '',
|
||||
isActive: true
|
||||
})
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const openEditModal = (coupon: Coupon) => {
|
||||
setEditingCoupon({
|
||||
...coupon,
|
||||
expiresAt: coupon.expiresAt ? coupon.expiresAt.split('T')[0] : ''
|
||||
})
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const formatDiscount = (type: string, value: number) => {
|
||||
if (type === 'PERCENTAGE') return `${value}% OFF`
|
||||
if (type === 'FIXED_AMOUNT') return `฿${value} OFF`
|
||||
if (type === 'FREE_SHIPPING') return `FREE SHIPPING`
|
||||
return `${value}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-fade-in">
|
||||
{!isModalOpen ? (
|
||||
<>
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Coupons</h1>
|
||||
<p className="text-slate-500 text-sm">Manage discount codes and promotional campaigns.</p>
|
||||
</div>
|
||||
<button onClick={openAddModal} className="btn-primary flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Create Coupon
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="admin-card h-48 bg-white/5 animate-pulse" />
|
||||
))
|
||||
) : (
|
||||
coupons.map((coupon) => (
|
||||
<div key={coupon.id} className={`admin-card group relative overflow-hidden ${!coupon.isActive ? 'opacity-60 grayscale' : ''}`}>
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-3 rounded-2xl ${coupon.isActive ? 'bg-cyan-glow/10 text-cyan-glow' : 'bg-slate-800 text-slate-500'}`}>
|
||||
<Ticket className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-white tracking-widest">{coupon.code}</h3>
|
||||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md uppercase tracking-widest ${
|
||||
coupon.type === 'PERCENTAGE' ? 'bg-indigo-500/20 text-indigo-400' :
|
||||
coupon.type === 'FREE_SHIPPING' ? 'bg-emerald-500/20 text-emerald-400' :
|
||||
'bg-amber-500/20 text-amber-400'
|
||||
}`}>
|
||||
{formatDiscount(coupon.type, coupon.value)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleActive(coupon.id, coupon.isActive)}
|
||||
className={`p-2 rounded-lg bg-night-800 border transition-all ${
|
||||
coupon.isActive
|
||||
? 'border-cyan-glow/30 text-cyan-glow hover:bg-cyan-glow/10'
|
||||
: 'border-rose-500/30 text-rose-500 hover:bg-rose-500/10'
|
||||
}`}
|
||||
title={coupon.isActive ? 'Deactivate' : 'Activate'}
|
||||
>
|
||||
{coupon.isActive ? <Power className="w-4 h-4" /> : <PowerOff className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openEditModal(coupon)}
|
||||
className="p-2 rounded-lg bg-night-800 border border-white/5 text-slate-500 hover:text-cyan-glow hover:border-cyan-glow/30 transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(coupon.id)}
|
||||
className="p-2 rounded-lg bg-night-800 border border-white/5 text-slate-500 hover:text-rose-500 hover:border-rose-500/30 transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||
<div className="space-y-1">
|
||||
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Usage</p>
|
||||
<p className="text-white font-medium">{coupon.usedCount} / {coupon.usageLimit ? coupon.usageLimit : '∞'}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Min Purchase</p>
|
||||
<p className="text-white font-medium">฿{coupon.minPurchase}</p>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Expires</p>
|
||||
<p className="text-white font-medium">{coupon.expiresAt ? new Date(coupon.expiresAt).toLocaleDateString() : 'No expiration'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{coupon.isActive && <div className="absolute top-0 right-0 w-32 h-32 bg-cyan-glow/5 blur-3xl rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
editingCoupon && (
|
||||
<div className="w-full max-w-2xl mx-auto bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-fade-in">
|
||||
<div className="p-6 border-b border-white/5 flex flex-shrink-0 items-center justify-between bg-white/[0.02]">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<Ticket className="w-5 h-5 text-cyan-glow" />
|
||||
{editingCoupon.id ? 'Edit Coupon' : 'Create Coupon'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-500 hover:text-white transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Coupon Code</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingCoupon.code || ''}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, code: e.target.value.toUpperCase() })}
|
||||
className="admin-input w-full uppercase tracking-widest font-bold"
|
||||
placeholder="e.g. SUMMER2026"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Discount Type</label>
|
||||
<select
|
||||
value={editingCoupon.type || 'PERCENTAGE'}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, type: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
>
|
||||
<option value="PERCENTAGE">Percentage (%)</option>
|
||||
<option value="FIXED_AMOUNT">Fixed Amount (฿)</option>
|
||||
<option value="FREE_SHIPPING">Free Shipping</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">
|
||||
Discount Value {editingCoupon.type === 'PERCENTAGE' ? '(%)' : editingCoupon.type === 'FIXED_AMOUNT' ? '(฿)' : ''}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
required={editingCoupon.type !== 'FREE_SHIPPING'}
|
||||
disabled={editingCoupon.type === 'FREE_SHIPPING'}
|
||||
value={editingCoupon.value || 0}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, value: parseFloat(e.target.value) })}
|
||||
className="admin-input w-full disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Minimum Purchase (฿)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editingCoupon.minPurchase || 0}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, minPurchase: parseFloat(e.target.value) })}
|
||||
className="admin-input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Max Discount (฿)</label>
|
||||
<input
|
||||
type="number"
|
||||
disabled={editingCoupon.type !== 'PERCENTAGE'}
|
||||
value={editingCoupon.maxDiscount || ''}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, maxDiscount: e.target.value ? parseFloat(e.target.value) : null })}
|
||||
className="admin-input w-full disabled:opacity-50"
|
||||
placeholder="Leave empty for no limit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Usage Limit</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editingCoupon.usageLimit || ''}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, usageLimit: e.target.value ? parseInt(e.target.value) : null })}
|
||||
className="admin-input w-full"
|
||||
placeholder="Leave empty for unlimited"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Expiration Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={editingCoupon.expiresAt || ''}
|
||||
onChange={(e) => setEditingCoupon({ ...editingCoupon, expiresAt: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/5 flex flex-col md:flex-row gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 btn-primary py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
{editingCoupon.id ? 'Save Changes' : 'Create Coupon'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_BASE } from '@irelax/types'
|
||||
|
||||
interface Customer {
|
||||
id: number
|
||||
lineUserId: string
|
||||
displayName: string | null
|
||||
pictureUrl: string | null
|
||||
email: string | null
|
||||
phone: string | null
|
||||
createdAt: string
|
||||
_count?: {
|
||||
orders: number
|
||||
}
|
||||
}
|
||||
|
||||
export default function CustomersPage() {
|
||||
const [customers, setCustomers] = useState<Customer[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchCustomers()
|
||||
}, [])
|
||||
|
||||
const fetchCustomers = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/customers`)
|
||||
const data = await res.json()
|
||||
setCustomers(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="p-8 animate-pulse text-slate-400">Loading Customers...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight">LINE Customers</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50 border-b border-slate-200 text-slate-500 uppercase text-xs tracking-wider">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-semibold">Customer</th>
|
||||
<th className="px-6 py-4 font-semibold">Contact Info</th>
|
||||
<th className="px-6 py-4 font-semibold">Orders</th>
|
||||
<th className="px-6 py-4 font-semibold">Registered Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{customers.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-8 text-center text-slate-500">
|
||||
No customers found yet.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
customers.map(customer => (
|
||||
<tr key={customer.id} className="hover:bg-slate-50/80 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName || 'User'}&background=random`}
|
||||
alt={customer.displayName || 'User'}
|
||||
className="w-10 h-10 rounded-full object-cover border border-slate-200"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-900">{customer.displayName || 'No Name'}</p>
|
||||
<p className="text-xs text-slate-500 truncate w-32" title={customer.lineUserId}>UID: {customer.lineUserId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-slate-700">{customer.phone || '-'}</p>
|
||||
<p className="text-slate-500 text-xs">{customer.email || '-'}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{customer._count?.orders || 0} Orders
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-600">
|
||||
{new Date(customer.createdAt).toLocaleDateString('th-TH')}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Package, Tag, Layers, TrendingUp } from 'lucide-react'
|
||||
import { API_BASE, type Category, type Product } from '@irelax/types'
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stats, setStats] = useState({
|
||||
products: 0,
|
||||
categories: 0,
|
||||
scents: 0,
|
||||
avgPrice: 0
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [catRes, prodRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/categories`),
|
||||
fetch(`${API_BASE}/products`)
|
||||
])
|
||||
const cats: Category[] = await catRes.json()
|
||||
const prods: Product[] = await prodRes.json()
|
||||
|
||||
const scents = new Set(prods.map(p => p.scent.id)).size
|
||||
const totalProducts = prods.length
|
||||
const avgPrice = prods.reduce((acc, curr) => acc + (curr.price || 0), 0) / (totalProducts || 1)
|
||||
|
||||
setStats({
|
||||
products: totalProducts,
|
||||
categories: cats.length,
|
||||
scents,
|
||||
avgPrice
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Dashboard data error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const cards = [
|
||||
{ label: 'Total Products', value: stats.products, icon: Package, color: 'text-blue-400' },
|
||||
{ label: 'Active Categories', value: stats.categories, icon: Layers, color: 'text-cyan-glow' },
|
||||
{ label: 'Unique Scents', value: stats.scents, icon: Tag, color: 'text-purple-400' },
|
||||
{ label: 'Avg Unit Price', value: `฿${Math.round(stats.avgPrice).toLocaleString()}`, icon: TrendingUp, color: 'text-emerald-400' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-fade-in">
|
||||
<div className="flex justify-between items-end">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">System Overview</h1>
|
||||
<p className="text-slate-500 text-sm">Real-time stats from the iRelax platform.</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs uppercase tracking-widest text-slate-500">Last updated</p>
|
||||
<p className="text-sm font-medium text-silver-metallic">{new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
{cards.map((card) => (
|
||||
<div key={card.label} className="admin-card admin-card-hover group">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className={`p-3 rounded-xl bg-night-800 border border-white/5 group-hover:border-cyan-glow/30 transition-colors`}>
|
||||
<card.icon className={`w-6 h-6 ${card.color}`} />
|
||||
</div>
|
||||
<span className="text-[10px] uppercase font-bold tracking-[0.2em] text-silver-metallic/30">Stats</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{loading ? (
|
||||
<div className="h-9 w-2/3 bg-white/5 animate-pulse rounded" />
|
||||
) : (
|
||||
<p className="text-3xl font-bold text-white tracking-tight">{card.value}</p>
|
||||
)}
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-slate-500">{card.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Placeholder for recent activities */}
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div className="admin-card space-y-6">
|
||||
<h3 className="font-bold text-lg">Inventory Summary</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Dummy chart-like bars */}
|
||||
{[
|
||||
{ label: 'Oil Ozone', val: 0.8 },
|
||||
{ label: 'Aroma Oil', val: 0.6 },
|
||||
{ label: 'Reed Diffuser', val: 0.4 },
|
||||
{ label: 'Car Diffuser', val: 0.2 },
|
||||
].map(i => (
|
||||
<div key={i.label} className="space-y-2">
|
||||
<div className="flex justify-between text-xs font-bold uppercase tracking-widest">
|
||||
<span className="text-slate-400">{i.label}</span>
|
||||
<span className="text-cyan-glow">{Math.round(i.val * 100)}% Stock</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full bg-night-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-cyan-glow shadow-[0_0_10px_rgba(102,252,241,0.5)] rounded-full transition-all duration-1000"
|
||||
style={{ width: `${i.val * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card space-y-6">
|
||||
<h3 className="font-bold text-lg">System Health</h3>
|
||||
<div className="space-y-5">
|
||||
{[
|
||||
{ label: 'API Backend', status: 'Healthy', time: '12ms' },
|
||||
{ label: 'Database Service', status: 'Healthy', time: '4ms' },
|
||||
{ label: 'Admin App Bundle', status: 'Optimized', time: '128kb' },
|
||||
].map(i => (
|
||||
<div key={i.label} className="flex items-center justify-between p-4 bg-night-950/50 rounded-xl border border-white/5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-bold text-white">{i.label}</p>
|
||||
<p className="text-[10px] uppercase font-bold text-emerald-500 tracking-widest">{i.status}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-slate-500">Performance</p>
|
||||
<p className="text-sm font-bold text-white">{i.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, DollarSign, Package, BadgePercent, ShoppingCart, Loader2, Store, Calendar, Globe2
|
||||
} from 'lucide-react';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// We adjust TikTok to Cyan/Pink mix or just Cyan, Lazada to Blue, Shopee to Orange.
|
||||
const COLORS = {
|
||||
Shopee: '#f97316',
|
||||
TikTok: '#06b6d4',
|
||||
Lazada: '#3b82f6',
|
||||
};
|
||||
|
||||
// ── Date range helpers ────────────────────────────────────────────────────────
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
function toISO(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISO(now);
|
||||
if (preset === '7d') { const f = new Date(now); f.setDate(f.getDate() - 6); return { from: toISO(f), to: today }; }
|
||||
if (preset === '30d') { const f = new Date(now); f.setDate(f.getDate() - 29); return { from: toISO(f), to: today }; }
|
||||
if (preset === '90d') { const f = new Date(now); f.setDate(f.getDate() - 89); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'thisMonth') { const f = new Date(now.getFullYear(), now.getMonth(), 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastMonth') { const f = new Date(now.getFullYear(), now.getMonth() - 1, 1); const t = new Date(now.getFullYear(), now.getMonth(), 0); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'year') { const f = new Date(now.getFullYear(), 0, 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastYear') { const f = new Date(now.getFullYear() - 1, 0, 1); const t = new Date(now.getFullYear() - 1, 11, 31); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'last12m') { const f = new Date(now); f.setFullYear(f.getFullYear() - 1); return { from: toISO(f), to: today }; }
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
function fmt(val: number, dec = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||
}
|
||||
function fmtB(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function KPICard({ label, value, sub, color = 'text-white', icon: Icon }: { label: string; value: string; sub?: string; color?: string; icon: any }) {
|
||||
return (
|
||||
<div className="relative bg-white/5 border border-white/10 rounded-2xl p-5 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]"><Icon className="w-14 h-14" /></div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-[#0f172a] border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1.5 min-w-[160px]">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white ml-auto">
|
||||
{typeof p.value === 'number' ? (p.value > 1000 ? fmtB(p.value) : fmt(p.value)) : p.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function GlobalAnalyticsPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const loadData = async (from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
|
||||
try {
|
||||
const [shopeeRes, tiktokRes, lazadaRes] = await Promise.all([
|
||||
fetch(`${API_URL}/api/shopee/analytics-overview${qs}`, { headers: authHeaders() }).then(r => r.ok ? r.json() : null),
|
||||
fetch(`${API_URL}/api/tiktok-order/analytics-overview${qs}`, { headers: authHeaders() }).then(r => r.ok ? r.json() : null),
|
||||
fetch(`${API_URL}/api/lazada-order/analytics-overview${qs}`, { headers: authHeaders() }).then(r => r.ok ? r.json() : null),
|
||||
]);
|
||||
|
||||
const shopeeData = shopeeRes?.totals || shopeeRes?.summary || {};
|
||||
const shopee = {
|
||||
totalRevenue: shopeeData.totalRevenue || 0,
|
||||
totalOrders: shopeeData.totalOrders || 0,
|
||||
cancelledOrders: shopeeData.totalCancelled || shopeeData.cancelledOrders || 0,
|
||||
completedOrders: shopeeData.completedOrders || ((shopeeData.totalOrders || 0) - (shopeeData.totalCancelled || 0)),
|
||||
totalShipping: shopeeData.totalShipping || 0,
|
||||
};
|
||||
|
||||
const tiktok = tiktokRes?.summary || { totalRevenue: 0, totalOrders: 0, completedOrders: 0, cancelledOrders: 0, cancelRate: 0, totalShipping: 0 };
|
||||
const lazada = lazadaRes?.summary || { totalRevenue: 0, totalOrders: 0, completedOrders: 0, cancelledOrders: 0, cancelRate: 0, totalShipping: 0 };
|
||||
|
||||
const totalRevenue = shopee.totalRevenue + tiktok.totalRevenue + lazada.totalRevenue;
|
||||
const totalOrders = shopee.totalOrders + tiktok.totalOrders + lazada.totalOrders;
|
||||
const completedOrders = shopee.completedOrders + tiktok.completedOrders + lazada.completedOrders;
|
||||
const cancelledOrders = shopee.cancelledOrders + tiktok.cancelledOrders + lazada.cancelledOrders;
|
||||
const totalShipping = shopee.totalShipping + tiktok.totalShipping + lazada.totalShipping;
|
||||
|
||||
const platforms = [
|
||||
{ name: 'Shopee', revenue: shopee.totalRevenue, orders: shopee.totalOrders },
|
||||
{ name: 'TikTok', revenue: tiktok.totalRevenue, orders: tiktok.totalOrders },
|
||||
{ name: 'Lazada', revenue: lazada.totalRevenue, orders: lazada.totalOrders },
|
||||
].sort((a, b) => b.revenue - a.revenue);
|
||||
|
||||
// Merge monthly
|
||||
const monthMap = new Map<string, any>();
|
||||
const addMonthly = (arr: any[], platform: string) => {
|
||||
if (!arr) return;
|
||||
arr.forEach(m => {
|
||||
if (!monthMap.has(m.month)) monthMap.set(m.month, { month: m.month, Shopee: 0, TikTok: 0, Lazada: 0 });
|
||||
const rev = m.revenue ?? m.totalRevenue ?? 0;
|
||||
monthMap.get(m.month)[platform] += rev;
|
||||
});
|
||||
};
|
||||
addMonthly(shopeeRes?.monthly, 'Shopee');
|
||||
addMonthly(tiktokRes?.monthly, 'TikTok');
|
||||
addMonthly(lazadaRes?.monthly, 'Lazada');
|
||||
|
||||
const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
|
||||
|
||||
setData({
|
||||
platforms,
|
||||
monthly,
|
||||
summary: {
|
||||
totalRevenue, totalOrders, completedOrders, cancelledOrders, totalShipping,
|
||||
cancelRate: totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0,
|
||||
aov: completedOrders > 0 ? totalRevenue / completedOrders : 0,
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { loadData('', ''); }, []);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(dateFrom, dateTo);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
|
||||
<span className="text-slate-500">กำลังรวมข้อมูลระดับ Global (ทุกแพลตฟอร์ม)...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<Globe2 className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'เกิดข้อผิดพลาดในการโหลดข้อมูล'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { platforms, monthly, summary } = data;
|
||||
const pieData = platforms.filter((p: any) => p.revenue > 0).map((p: any) => ({ name: p.name, value: p.revenue }));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-emerald-500/30 to-teal-500/20 border border-white/10 flex items-center justify-center">
|
||||
<Globe2 className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Global Overview</p>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">ภาพรวมทุกแพลตฟอร์ม</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
|
||||
preset === p.key
|
||||
? 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30'
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-emerald-500/40 focus:outline-none"
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-emerald-500/40 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className="px-4 py-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-medium hover:bg-emerald-500/20 transition-all"
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData('', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grand KPI Row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<KPICard label="ยอดขายรวมระดับบริษัท" value={fmtB(summary.totalRevenue)} sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} color="text-emerald-400" icon={DollarSign} />
|
||||
<KPICard label="AOV รวม (เฉลี่ย)" value={fmtB(summary.aov)} sub={`จากออเดอร์สำเร็จ ${fmt(summary.completedOrders)}`} color="text-blue-400" icon={ShoppingCart} />
|
||||
<KPICard label="อัตรายกเลิกรวม" value={`${fmt(summary.cancelRate, 1)}%`} sub={`${fmt(summary.cancelledOrders)} รายการยกเลิก`} color="text-rose-400" icon={BadgePercent} />
|
||||
<KPICard label="ค่าจัดส่งรวม" value={fmtB(summary.totalShipping)} sub="ค่าจัดส่งทุกแพลตฟอร์ม" color="text-amber-400" icon={Package} />
|
||||
</div>
|
||||
|
||||
{/* Revenue Share Pie + Comparison table */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Globe2 className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนยอดขายตามแพลตฟอร์ม</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-[220px] flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={pieData} cx="50%" cy="50%" innerRadius={55} outerRadius={90} dataKey="value" nameKey="name" stroke="none" paddingAngle={3}>
|
||||
{pieData.map((entry: any, i: number) => (
|
||||
<Cell key={i} fill={COLORS[entry.name as keyof typeof COLORS] || '#94a3b8'} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }}
|
||||
formatter={(v: any) => fmtB(v)} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{platforms.map((p: any) => {
|
||||
if(p.revenue === 0) return null;
|
||||
const c = COLORS[p.name as keyof typeof COLORS];
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<div key={p.name} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: c }} />
|
||||
<span className="text-slate-300 font-medium w-16">{p.name}</span>
|
||||
<span className="font-mono text-white">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Store className="w-5 h-5 text-slate-400" />
|
||||
<h2 className="text-lg font-bold text-white">เปรียบเทียบแพลตฟอร์ม</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left">แพลตฟอร์ม</th>
|
||||
<th className="pb-3 text-right">ยอดขาย</th>
|
||||
<th className="pb-3 text-right">สัดส่วน</th>
|
||||
<th className="pb-3 text-right">ออเดอร์</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{platforms.map((p: any) => {
|
||||
const c = COLORS[p.name as keyof typeof COLORS];
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={p.name} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: c }} />
|
||||
<span className="font-medium text-white">{p.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-right font-mono text-emerald-400 font-bold">{fmtB(p.revenue)}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-400">{fmt(pct, 1)}%</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300">{fmt(p.orders)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monthly Stacked Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-5 h-5 text-emerald-500" />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน (แยกตามแพลตฟอร์ม)</h2>
|
||||
</div>
|
||||
{monthly.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthly} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barCategoryGap="30%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="month" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => v >= 1000 ? `฿${(v / 1000).toFixed(0)}k` : `฿${v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} />
|
||||
{platforms.map((p: any, i: number) => {
|
||||
const isTop = i === platforms.length - 1; // Simplification
|
||||
return (
|
||||
<Bar key={p.name} dataKey={p.name} name={p.name} stackId="rev" fill={COLORS[p.name as keyof typeof COLORS]} radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]} />
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Plus,
|
||||
Trash2,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Image as ImageIcon,
|
||||
UploadCloud,
|
||||
Loader2,
|
||||
X,
|
||||
ExternalLink,
|
||||
ChevronRight,
|
||||
Info,
|
||||
ClipboardPaste
|
||||
} from 'lucide-react'
|
||||
import { API_BASE, type Hero } from '@irelax/types'
|
||||
|
||||
export default function HeroSettingsPage() {
|
||||
const [heroes, setHeroes] = useState<Hero[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [editingHero, setEditingHero] = useState<Partial<Hero> | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/hero/all`)
|
||||
const data = await res.json()
|
||||
setHeroes(data)
|
||||
} catch (err) {
|
||||
console.error('Fetch error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!editingHero) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/hero`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingHero)
|
||||
})
|
||||
if (res.ok) {
|
||||
setIsModalOpen(false)
|
||||
fetchData()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Save error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleActivate = async (id: number) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/hero/${id}/activate`, {
|
||||
method: 'PATCH'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Activate error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this Hero entry?')) return
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/hero/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (res.ok) {
|
||||
fetchData()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
setIsUploading(true)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.url) {
|
||||
setEditingHero(prev => ({ ...prev, imageUrl: data.url }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteFromClipboard = async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read()
|
||||
for (const item of items) {
|
||||
const imageTypes = item.types.filter(t => t.startsWith('image/'))
|
||||
for (const type of imageTypes) {
|
||||
const blob = await item.getType(type)
|
||||
const file = new File([blob], 'pasted-image.png', { type })
|
||||
handleUpload(file)
|
||||
return
|
||||
}
|
||||
}
|
||||
alert('No image found in clipboard')
|
||||
} catch (err) {
|
||||
console.error('Clipboard error:', err)
|
||||
alert('Please allow clipboard access or copy an image first.')
|
||||
}
|
||||
}
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingHero({
|
||||
tagline: 'A New Paradigm of Calm',
|
||||
title: 'Inhale [Serenity]',
|
||||
description: 'Crafting premium sensory experiences that transform your space into a sanctuary of peace.',
|
||||
imageUrl: '/images/hero.png',
|
||||
buttonText: 'Explore Collection',
|
||||
buttonLink: '/shop',
|
||||
isActive: false
|
||||
})
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const openEditModal = (hero: Hero) => {
|
||||
setEditingHero(hero)
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{!isModalOpen ? (
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Hero Management</h1>
|
||||
<p className="text-slate-500 text-sm">Control the first impression of your sanctuary.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openAddModal}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create New Hero
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="h-48 admin-card bg-white/5 animate-pulse" />
|
||||
))
|
||||
) : heroes.length === 0 ? (
|
||||
<div className="admin-card py-20 text-center space-y-4">
|
||||
<LayoutDashboard className="w-12 h-12 text-slate-700 mx-auto" />
|
||||
<p className="text-slate-500 italic">No hero entries found. Create your first one!</p>
|
||||
</div>
|
||||
) : (
|
||||
heroes.map((hero) => (
|
||||
<div
|
||||
key={hero.id}
|
||||
className={`admin-card overflow-hidden group border-l-4 transition-all duration-500 ${
|
||||
hero.isActive ? 'border-l-cyan-glow' : 'border-l-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Preview Image */}
|
||||
<div className="lg:w-96 aspect-video rounded-2xl overflow-hidden border border-white/5 bg-night-950/50 flex-shrink-0 relative">
|
||||
<img
|
||||
src={hero.imageUrl}
|
||||
className={`w-full h-full object-cover transition-all duration-700 ${hero.isActive ? 'grayscale-0' : 'grayscale opacity-40'}`}
|
||||
alt="Hero Preview"
|
||||
/>
|
||||
{!hero.isActive && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-night-950/40 backdrop-blur-[2px]">
|
||||
<span className="px-3 py-1 bg-night-950 border border-white/10 rounded-full text-[10px] font-bold uppercase tracking-widest text-slate-500">Inactive</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col justify-between py-2">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="px-2 py-1 bg-cyan-glow/10 text-cyan-glow text-[10px] font-bold uppercase tracking-widest border border-cyan-glow/20 rounded-md">
|
||||
{hero.tagline}
|
||||
</span>
|
||||
{hero.isActive && (
|
||||
<span className="flex items-center gap-1.5 text-cyan-glow text-[10px] font-bold uppercase tracking-widest animate-pulse">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
Live on Site
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600 font-mono">ID: HERO-{hero.id.toString().padStart(3, '0')}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-bold flex items-baseline gap-2">
|
||||
{hero.title.split('[').map((part, i) => {
|
||||
if (part.includes(']')) {
|
||||
const [accent, rest] = part.split(']')
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-cyan-glow italic font-serif">{accent}</span>
|
||||
{rest}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span key={i}>{part}</span>
|
||||
})}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm leading-relaxed max-w-2xl line-clamp-2">
|
||||
{hero.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 pt-2">
|
||||
<div className="flex items-center gap-2 group/btn">
|
||||
<span className="w-8 h-px bg-white/10 group-hover/btn:bg-cyan-glow/30 transition-colors" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-cyan-glow">{hero.buttonText}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-600 text-[10px] font-bold">
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{hero.buttonLink}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-6 lg:pt-0">
|
||||
{!hero.isActive && (
|
||||
<button
|
||||
onClick={() => handleActivate(hero.id)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-white text-xs font-bold transition-all border border-white/5"
|
||||
>
|
||||
<Circle className="w-4 h-4" />
|
||||
Set Active
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => openEditModal(hero)}
|
||||
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white transition-all border border-white/5"
|
||||
title="Edit Hero"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(hero.id)}
|
||||
className="p-2.5 rounded-xl bg-rose-500/10 hover:bg-rose-500 text-rose-500 hover:text-white transition-all border border-rose-500/10"
|
||||
title="Delete Entry"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
editingHero && (
|
||||
<div className="w-full bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-fade-in">
|
||||
<div className="p-6 border-b border-white/5 flex flex-shrink-0 items-center justify-between bg-white/[0.02]">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<LayoutDashboard className="w-5 h-5 text-cyan-glow" />
|
||||
{editingHero.id ? 'Edit Hero Section' : 'Create New Hero'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-500 hover:text-white transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Image Upload Area */}
|
||||
<div className="md:col-span-2 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Background Image</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePasteFromClipboard}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow text-[10px] font-bold uppercase tracking-wider transition-all border border-cyan-glow/20"
|
||||
>
|
||||
<ClipboardPaste className="w-3.5 h-3.5" />
|
||||
Paste Image
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-dashed border-white/5 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all"
|
||||
onClick={() => document.getElementById('hero-upload')?.click()}
|
||||
>
|
||||
{editingHero.imageUrl ? (
|
||||
<>
|
||||
<img src={editingHero.imageUrl} className="w-full h-full object-cover" alt="Hero Background" />
|
||||
<div className="absolute inset-0 bg-night-950/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center backdrop-blur-sm">
|
||||
<UploadCloud className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center space-y-2">
|
||||
<ImageIcon className="w-10 h-10 text-slate-700 mx-auto" />
|
||||
<p className="text-xs text-slate-500 font-bold uppercase tracking-widest">Select Image</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 bg-night-950/60 flex items-center justify-center">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
id="hero-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) handleUpload(file)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-[10px] text-slate-500">
|
||||
<Info className="w-3 h-3 flex-shrink-0 mt-0.5" />
|
||||
<p>Recommended size: 1920x1080px. High quality but optimized for web.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Fields */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Tagline</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingHero.tagline || ''}
|
||||
onChange={(e) => setEditingHero({ ...editingHero, tagline: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. A New Paradigm of Calm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingHero.title || ''}
|
||||
onChange={(e) => setEditingHero({ ...editingHero, title: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. Inhale [Serenity]"
|
||||
/>
|
||||
<p className="text-[9px] text-slate-600 font-medium tracking-wide">Use [brackets] for the accent serif font.</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Description</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
required
|
||||
value={editingHero.description || ''}
|
||||
onChange={(e) => setEditingHero({ ...editingHero, description: e.target.value })}
|
||||
className="admin-input w-full resize-none"
|
||||
placeholder="Tell the story of your sanctuary..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Button Text</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingHero.buttonText || ''}
|
||||
onChange={(e) => setEditingHero({ ...editingHero, buttonText: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. Explore Collection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Button Link</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingHero.buttonLink || ''}
|
||||
onChange={(e) => setEditingHero({ ...editingHero, buttonLink: e.target.value })}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. /shop"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/5 flex flex-col md:flex-row gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isUploading}
|
||||
className="flex-1 btn-primary py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
{editingHero.id ? 'Update Hero Settings' : 'Create Hero Entry'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-8 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-xl transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, LineChart, Line, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, ShoppingBag, DollarSign, ReceiptText, Package,
|
||||
ArrowLeft, BadgePercent, ShoppingCart, XCircle, CheckCircle2, Loader2,
|
||||
Calendar, Store,
|
||||
} from 'lucide-react';
|
||||
import { LAZADA_SHOPS } from './LazadaOrderPage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#eab308', '#f97316', '#22d3ee', '#10b981'];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
function fmt(val: number, decimals = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(val);
|
||||
}
|
||||
function fmtCurrency(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function toISODate(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISODate(now);
|
||||
if (preset === '7d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 6);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '30d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 29);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '90d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 89);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'thisMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const to = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'year') {
|
||||
const from = new Date(now.getFullYear(), 0, 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastYear') {
|
||||
const from = new Date(now.getFullYear() - 1, 0, 1);
|
||||
const to = new Date(now.getFullYear() - 1, 11, 31);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'last12m') {
|
||||
const from = new Date(now); from.setFullYear(from.getFullYear() - 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function StatCard({
|
||||
label, value, sub, icon: Icon, color = 'text-white', bgColor = 'from-white/5',
|
||||
}: { label: string; value: string | number; sub?: string; icon: any; color?: string; bgColor?: string }) {
|
||||
return (
|
||||
<div className={`relative bg-gradient-to-br ${bgColor} to-transparent border border-white/10 rounded-2xl p-5 overflow-hidden`}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]">
|
||||
<Icon className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-night-900 border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white">{typeof p.value === 'number' && p.name?.includes('฿') ? fmtCurrency(p.value) : fmt(p.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LazadaAnalyticsPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Shop selector
|
||||
const [shop, setShop] = useState('hom-aroma');
|
||||
const shopCfg = LAZADA_SHOPS.find(s => s.id === shop) ?? LAZADA_SHOPS[0];
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const buildUrl = useCallback((s: string, from: string, to: string) => {
|
||||
const params = new URLSearchParams({ shop: s });
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
return `${API_URL}/api/lazada-order/analytics?${params}`;
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback((s: string, from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
fetch(buildUrl(s, from, to), { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
}, [buildUrl]);
|
||||
|
||||
// Reload on shop or initial mount
|
||||
useEffect(() => { loadData(shop, dateFrom, dateTo); }, [shop]);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(shop, range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(shop, dateFrom, dateTo);
|
||||
};
|
||||
|
||||
const handleShopChange = (s: string) => {
|
||||
setShop(s);
|
||||
// reset dates so we don't carry over filter from previous shop
|
||||
setPreset('all'); setDateFrom(''); setDateTo('');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className={`w-8 h-8 ${shopCfg.color} animate-spin`} />
|
||||
<span className="text-slate-500">กำลังคำนวณข้อมูลวิเคราะห์ Lazada...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<ShoppingBag className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ไม่มีข้อมูล — กรุณา Import ไฟล์ Lazada ก่อน'}</p>
|
||||
<Link to="/lazada-order" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 hover:text-white text-sm">
|
||||
<ArrowLeft className="w-4 h-4" /> กลับหน้า Lazada Orders
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, monthly, statusBreakdown, paymentBreakdown, topProducts } = data;
|
||||
|
||||
// Prepare monthly chart
|
||||
const monthlyChart = monthly.map((m: any) => ({
|
||||
...m,
|
||||
label: m.month,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/lazada-order" className="p-2 rounded-xl border border-white/10 hover:bg-white/5 text-slate-400 hover:text-white transition-all">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<Link to="/lazada-order" className={`hover:${shopCfg.color} transition-colors`}>Lazada</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white">Analytics</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
|
||||
<TrendingUp className={`w-6 h-6 ${shopCfg.color}`} />
|
||||
วิเคราะห์ยอดขาย —
|
||||
<span className={shopCfg.color}>{shopCfg.label}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shop Switcher */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Store className="w-4 h-4 text-slate-500 mr-2" />
|
||||
{LAZADA_SHOPS.map(s => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => handleShopChange(s.id)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold border transition-all ${
|
||||
shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
preset === p.key
|
||||
? `${shopCfg.activeBg} ${shopCfg.color} ${shopCfg.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={e => setDateFrom(e.target.value)}
|
||||
className={`px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:${shopCfg.activeBorder} focus:outline-none`}
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={e => setDateTo(e.target.value)}
|
||||
className={`px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:${shopCfg.activeBorder} focus:outline-none`}
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className={`px-4 py-1.5 rounded-lg ${shopCfg.activeBg} ${shopCfg.activeBorder} ${shopCfg.color} text-xs font-medium hover:opacity-80 transition-all`}
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active filter badge */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData(shop, '', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Row 1 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="ยอดขายรวม (Paid Price)" value={fmtCurrency(summary.totalRevenue)} icon={DollarSign} color="text-emerald-400" bgColor="from-emerald-500/10" sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} />
|
||||
<StatCard label="ยอดสุทธิ (Net from shipping)" value={fmtCurrency(summary.netRevenue)} icon={TrendingUp} color="text-cyan-glow" bgColor="from-cyan-500/10" sub={`รายได้หลังหักค่าส่งจัดส่ง`} />
|
||||
<StatCard label="ส่วนลด Platform (Lazada)" value={fmtCurrency(summary.totalPlatformDisc)} icon={BadgePercent} color="text-purple-400" bgColor="from-purple-500/10" sub="Lazada อุดหนุนส่วนลดให้" />
|
||||
<StatCard label="ส่วนลดร้านค้า (เราออก)" value={fmtCurrency(summary.totalSellerDisc)} icon={ReceiptText} color="text-rose-400" bgColor="from-rose-500/10" sub="ส่วนลดที่เราให้ลูกค้า" />
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Row 2 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="คำสั่งซื้อสำเร็จ" value={fmt(summary.completedOrders)} icon={CheckCircle2} color="text-emerald-400" sub={`จากทั้งหมด ${fmt(summary.totalOrders)}`} />
|
||||
<StatCard label="คำสั่งซื้อยกเลิก" value={fmt(summary.cancelledOrders)} icon={XCircle} color="text-rose-400" sub={`${fmt(summary.cancelRate, 1)}% ของทั้งหมด`} />
|
||||
<StatCard label="ค่าจัดส่งรวม" value={fmtCurrency(summary.totalShipping)} icon={Package} color="text-amber-400" sub="ค่าจัดส่งทั้งหมด" />
|
||||
<StatCard label="AOV (เฉลี่ยต่อออเดอร์)" value={fmtCurrency(summary.aov)} icon={ShoppingCart} color="text-blue-400" sub="Average Order Value" />
|
||||
</div>
|
||||
|
||||
{/* Monthly Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className={`w-5 h-5 ${shopCfg.color}`} />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน</h2>
|
||||
</div>
|
||||
{monthlyChart.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barGap={3}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => `฿${v >= 1000 ? `${(v / 1000).toFixed(0)}k` : v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} />
|
||||
<Bar dataKey="revenue" name="฿ ยอดขายรวม" fill="#4ade80" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="platformDiscount" name="฿ ส่วนลด Platform" fill="#c084fc" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="sellerDiscount" name="฿ ส่วนลดร้านค้า" fill="#fb7185" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Orders per month line chart */}
|
||||
{monthlyChart.length > 1 && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ShoppingBag className="w-5 h-5 text-cyan-glow" />
|
||||
<h2 className="text-lg font-bold text-white">จำนวนคำสั่งซื้อรายเดือน</h2>
|
||||
</div>
|
||||
<div className="h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line type="monotone" dataKey="orders" name="คำสั่งซื้อ" stroke={shop === 'hom-aroma' ? '#3b82f6' : '#a78bfa'} strokeWidth={2} dot={{ r: 4 }} />
|
||||
<Line type="monotone" dataKey="items" name="จำนวนชิ้น" stroke="#818cf8" strokeWidth={2} dot={{ r: 4, fill: '#818cf8' }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status + Payment row */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Status Breakdown */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนสถานะคำสั่งซื้อ</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-center">
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={statusBreakdown} cx="50%" cy="50%" innerRadius={50} outerRadius={80} dataKey="count" nameKey="status" stroke="none" paddingAngle={2}>
|
||||
{statusBreakdown.map((_: any, i: number) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{statusBreakdown.map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: COLORS[i % COLORS.length] }} />
|
||||
<span className="text-slate-300 flex-1 truncate" title={s.status}>{s.status}</span>
|
||||
<span className="font-bold text-white">{s.count}</span>
|
||||
<span className="text-slate-500">({fmt((s.count / summary.totalOrders) * 100, 0)}%)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<DollarSign className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">ช่องทางการชำระเงิน</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{paymentBreakdown.map((p: any, i: number) => {
|
||||
const pct = summary.totalOrders > 0 ? (p.count / summary.totalOrders) * 100 : 0;
|
||||
return (
|
||||
<div key={i} className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-300 truncate max-w-[160px]" title={p.method}>{p.method || 'ไม่ระบุ'}</span>
|
||||
<span className="text-slate-400 ml-2">{p.count} ออเดอร์ · {fmt(pct, 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{ width: `${pct}%`, backgroundColor: COLORS[i % COLORS.length] }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี (by ยอดเงิน)</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/[0.03] border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3">#</th>
|
||||
<th className="px-4 py-3">สินค้า</th>
|
||||
<th className="px-4 py-3">ตัวเลือก</th>
|
||||
<th className="px-4 py-3 text-right">ชิ้น</th>
|
||||
<th className="px-4 py-3 text-right">ยอดรายได้</th>
|
||||
<th className="px-4 py-3 text-right">% จากยอดรวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<span className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-xs font-bold text-slate-400">
|
||||
{i + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{p.variation || '—'}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-slate-300">{fmt(p.qty)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">{fmtCurrency(p.revenue)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-16 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-emerald-400/60" style={{ width: `${Math.min(pct * 3, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Upload, ShoppingBag, CheckCircle, AlertTriangle, ChevronLeft, ChevronRight, Search, Loader2, DollarSign, Package, Trash2 } from 'lucide-react';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
export const LAZADA_SHOPS = [
|
||||
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-blue-500', activeBg: 'bg-blue-500/15', activeBorder: 'border-blue-500/40' },
|
||||
{ id: 'shop-01', label: 'Annlina', color: 'text-indigo-400', activeBg: 'bg-indigo-500/15', activeBorder: 'border-indigo-500/40' },
|
||||
{ id: 'pakasai', label: 'Pakasai', color: 'text-pink-400', activeBg: 'bg-pink-500/15', activeBorder: 'border-pink-500/40' },
|
||||
{ id: 'irelaxshop', label: 'Relax', color: 'text-yellow-400', activeBg: 'bg-yellow-500/15', activeBorder: 'border-yellow-500/40' },
|
||||
{ id: 'conner', label: 'Conner', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
||||
{ id: 'december', label: 'December', color: 'text-white', activeBg: 'bg-white/15', activeBorder: 'border-white/40' },
|
||||
|
||||
|
||||
];
|
||||
|
||||
function getShop(id: string) { return LAZADA_SHOPS.find(s => s.id === id) || LAZADA_SHOPS[0]; }
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
// ── Summary Cards ─────────────────────────────────────────────────────────────
|
||||
function SummaryCards({ shop, refreshKey }: { shop: string; refreshKey: number }) {
|
||||
const [data, setData] = useState({ orderCount: 0, totalAmount: 0, totalShipping: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/lazada-order/summary?shop=${shop}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(setData).catch(console.error);
|
||||
}, [shop, refreshKey]);
|
||||
|
||||
const cards = [
|
||||
{ label: 'คำสั่งซื้อทั้งหมด', value: data.orderCount.toLocaleString(), icon: ShoppingBag, color: 'text-white' },
|
||||
{ label: 'ยอดขายรวม (Gross)', value: `฿${data.totalAmount.toLocaleString(undefined, { minimumFractionDigits: 2 })}`, icon: DollarSign, color: 'text-emerald-400' },
|
||||
{ label: 'ค่าส่งรวม', value: `฿${data.totalShipping.toLocaleString(undefined, { minimumFractionDigits: 2 })}`, icon: Package, color: 'text-amber-400' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{cards.map(c => (
|
||||
<div key={c.label} className="bg-white/5 border border-white/10 rounded-2xl p-5 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.03]">
|
||||
<c.icon className="w-16 h-16" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">{c.label}</p>
|
||||
<c.icon className={`w-4 h-4 ${c.color}`} />
|
||||
</div>
|
||||
<p className={`text-xl font-bold ${c.color}`}>{c.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Orders Table ──────────────────────────────────────────────────────────────
|
||||
function OrdersTable({ shop, refreshKey }: { shop: string; refreshKey: number }) {
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => { setPage(1); }, [shop, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${API_URL}/api/lazada-order/orders?page=${page}&limit=20&shop=${shop}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setItems(d.items || []); setTotal(d.total || 0); setTotalPages(d.totalPages || 1); })
|
||||
.finally(() => setLoading(false));
|
||||
}, [shop, page, refreshKey]);
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
const status = s?.toLowerCase() || '';
|
||||
if (status.includes('delivered')) return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20';
|
||||
if (status.includes('shipped')) return 'bg-blue-500/15 text-blue-400 border-blue-500/20';
|
||||
if (status.includes('ready_to_ship')) return 'bg-amber-500/15 text-amber-400 border-amber-500/20';
|
||||
if (status.includes('canceled')) return 'bg-rose-500/15 text-rose-400 border-rose-500/20';
|
||||
return 'bg-white/5 text-slate-400 border-white/10';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-slate-400">ทั้งหมด <span className="text-white font-bold">{total.toLocaleString()}</span> คำสั่งซื้อ</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)} className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"><ChevronLeft className="w-4 h-4" /></button>
|
||||
<span className="text-slate-400 text-xs">{page} / {totalPages}</span>
|
||||
<button disabled={page >= totalPages} onClick={() => setPage(p => p + 1)} className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"><ChevronRight className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3 whitespace-nowrap">Order ID</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">สถานะ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">ผู้ซื้อ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">วันที่สร้าง</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">SKUs</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ยอดรวม (฿)</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ค่าส่ง (฿)</th>
|
||||
<th className="px-4 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={8} className="px-4 py-10 text-center text-slate-500">กำลังโหลด...</td></tr>
|
||||
) : items.length === 0 ? (
|
||||
<tr><td colSpan={8} className="px-4 py-16 text-center text-slate-500">ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน</td></tr>
|
||||
) : items.map((order) => (
|
||||
<tr key={order.orderNumber} className="border-b border-white/5 hover:bg-white/[0.04] transition-colors group">
|
||||
<td className="px-4 py-3 font-mono text-xs text-blue-400 font-bold">{order.orderNumber}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium whitespace-nowrap border ${statusColor(order.status)}`}>{order.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{order.customerName}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">{order.createTime?.slice(0, 16)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||
{order.items.map((i: any) => (
|
||||
<span key={i.id} className="text-[10px] px-1.5 py-0.5 bg-white/5 border border-white/10 rounded text-slate-300 truncate max-w-[150px]" title={i.itemName}>
|
||||
{i.itemName} {i.variation ? `(${i.variation})` : ''}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">฿{order.orderAmount.toLocaleString(undefined, { minimumFractionDigits: 2 })}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-slate-400">฿{order.shippingFee.toLocaleString(undefined, { minimumFractionDigits: 2 })}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<button className="text-[10px] px-2 py-1 bg-white/5 hover:bg-white/10 text-slate-300 rounded border border-white/10 transition-colors">รายละเอียด</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
export default function LazadaOrderPage() {
|
||||
const [shop, setShop] = useState(LAZADA_SHOPS[0].id);
|
||||
const [mode, setMode] = useState<'list' | 'import'>('list');
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// Import State
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState<any>(null);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
|
||||
const shopCfg = getShop(shop);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setFile(e.target.files[0]);
|
||||
setImportStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file) return;
|
||||
setImporting(true);
|
||||
setImportStatus(null);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const data = e.target?.result;
|
||||
const workbook = XLSX.read(data, { type: 'binary' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const rows = XLSX.utils.sheet_to_json(sheet);
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new Error("ไฟล์ไม่มีข้อมูล");
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_URL}/api/lazada-order/import`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ shop, rows })
|
||||
});
|
||||
const resData = await res.json();
|
||||
|
||||
if (res.ok && resData.success) {
|
||||
setImportStatus({ type: 'success', msg: resData.message });
|
||||
setFile(null);
|
||||
if (fileRef.current) fileRef.current.value = '';
|
||||
setRefreshKey(k => k + 1); // trigger list refresh
|
||||
} else {
|
||||
setImportStatus({ type: 'error', msg: resData.message || 'นำเข้าไม่สำเร็จ' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setImportStatus({ type: 'error', msg: err.message || 'เกิดข้อผิดพลาดในการอ่านไฟล์ Excel' });
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
setImportStatus({ type: 'error', msg: 'เกิดข้อผิดพลาดในการอ่านไฟล์' });
|
||||
setImporting(false);
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
};
|
||||
|
||||
const handleClearData = async () => {
|
||||
if (!confirm(`ยืนยันการลบข้อมูล Lazada ทั้งหมดของร้าน "${shopCfg.label}"?\nการกระทำนี้ไม่สามารถกู้คืนได้`)) return;
|
||||
setClearing(true);
|
||||
setImportStatus(null);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/lazada-order/clear?shop=${shop}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders()
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
setImportStatus({ type: 'success', msg: data.message });
|
||||
setRefreshKey(k => k + 1);
|
||||
} else {
|
||||
setImportStatus({ type: 'error', msg: data.message || 'ลบข้อมูลไม่สำเร็จ' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setImportStatus({ type: 'error', msg: err.message || 'เกิดข้อผิดพลาดในการเชื่อมต่อ' });
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<span className={shopCfg.color}>Lazada</span>
|
||||
<span>/</span>
|
||||
<span className="text-white">Orders</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
|
||||
คำสั่งซื้อ —
|
||||
<span className={shopCfg.color}>{shopCfg.label}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 bg-white/5 p-1 rounded-xl border border-white/10">
|
||||
{LAZADA_SHOPS.map(s => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => { setShop(s.id); setMode('list'); }}
|
||||
className={`px-4 py-1.5 rounded-lg text-sm font-bold border transition-all ${shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'border-transparent text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Tabs */}
|
||||
<div className="flex items-center gap-4 border-b border-white/10 pb-4">
|
||||
<button
|
||||
onClick={() => setMode('list')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-bold transition-all ${mode === 'list' ? `bg-white/10 text-white` : 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<Search className="w-4 h-4" /> ดูรายการคำสั่งซื้อ
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('import')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-bold transition-all ${mode === 'import' ? `${shopCfg.activeBg} ${shopCfg.color}` : 'text-slate-400 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<Upload className="w-4 h-4" /> อัปโหลด Excel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mode === 'import' ? (
|
||||
<div className="max-w-2xl">
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-2">นำเข้าไฟล์ยอดขาย Lazada (Excel)</h2>
|
||||
<p className="text-sm text-slate-400 mb-6">
|
||||
โหลดไฟล์ Export จาก Lazada Seller Center (ไฟล์ .xlsx หรือ .xls)
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx, .xls, .csv"
|
||||
ref={fileRef}
|
||||
onChange={handleFileChange}
|
||||
className="block w-full text-sm text-slate-400
|
||||
file:mr-4 file:py-2.5 file:px-4
|
||||
file:rounded-xl file:border-0
|
||||
file:text-sm file:font-bold
|
||||
file:bg-blue-500/10 file:text-blue-400
|
||||
hover:file:bg-blue-500/20 transition-all"
|
||||
/>
|
||||
|
||||
{importStatus && (
|
||||
<div className={`p-4 rounded-xl flex items-start gap-3 border ${importStatus.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400' : 'bg-rose-500/10 border-rose-500/20 text-rose-400'
|
||||
}`}>
|
||||
{importStatus.type === 'success' ? <CheckCircle className="w-5 h-5 mt-0.5" /> : <AlertTriangle className="w-5 h-5 mt-0.5" />}
|
||||
<span className="text-sm">{importStatus.msg}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!file || importing}
|
||||
className={`w-full flex justify-center items-center gap-2 py-3 px-4 rounded-xl font-bold transition-all ${!file || importing
|
||||
? 'bg-white/5 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20'
|
||||
}`}
|
||||
>
|
||||
{importing ? (
|
||||
<><Loader2 className="w-5 h-5 animate-spin" /> กำลังประมวลผล...</>
|
||||
) : (
|
||||
<><Upload className="w-5 h-5" /> นำเข้าข้อมูลเข้าฐานข้อมูล</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="pt-6 mt-6 border-t border-white/10">
|
||||
<p className="text-sm text-slate-400 mb-3">ต้องการล้างข้อมูลของร้านนี้เพื่ออัปโหลดใหม่ใช่หรือไม่?</p>
|
||||
<button
|
||||
onClick={handleClearData}
|
||||
disabled={clearing}
|
||||
className={`w-full flex justify-center items-center gap-2 py-3 px-4 rounded-xl font-bold transition-all ${
|
||||
clearing
|
||||
? 'bg-rose-500/10 text-rose-500/50 cursor-not-allowed'
|
||||
: 'bg-rose-500/10 text-rose-400 hover:bg-rose-500 hover:text-white border border-rose-500/20 hover:border-rose-500 hover:shadow-lg hover:shadow-rose-500/20'
|
||||
}`}
|
||||
>
|
||||
{clearing ? (
|
||||
<><Loader2 className="w-5 h-5 animate-spin" /> กำลังลบข้อมูล...</>
|
||||
) : (
|
||||
<><Trash2 className="w-5 h-5" /> ลบข้อมูลทั้งหมดของร้าน {shopCfg.label}</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<SummaryCards shop={shop} refreshKey={refreshKey} />
|
||||
<OrdersTable shop={shop} refreshKey={refreshKey} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, DollarSign, Package,
|
||||
BadgePercent, ShoppingCart, Loader2, Store, Calendar,
|
||||
} from 'lucide-react';
|
||||
import { LAZADA_SHOPS } from './LazadaOrderPage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// Color map per shop ID
|
||||
const SHOP_HEX: Record<string, string> = {
|
||||
'hom-aroma': '#3b82f6', // blue-500
|
||||
'shop-01': '#818cf8', // indigo-400
|
||||
'pakasai': '#f472b6', // pink-400
|
||||
'irelaxshop':'#facc15', // yellow-400
|
||||
'conner': '#94a3b8', // slate-400
|
||||
'december': '#cbd5e1', // slate-300
|
||||
};
|
||||
const PIE_FALLBACK = ['#3b82f6', '#818cf8', '#f472b6', '#facc15', '#94a3b8', '#cbd5e1'];
|
||||
|
||||
function shopHex(id: string, idx: number) {
|
||||
return SHOP_HEX[id] ?? PIE_FALLBACK[idx % PIE_FALLBACK.length];
|
||||
}
|
||||
function shopLabel(id: string) {
|
||||
return LAZADA_SHOPS.find(s => s.id === id)?.label ?? id;
|
||||
}
|
||||
|
||||
// ── Date range helpers ────────────────────────────────────────────────────────
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
function toISO(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISO(now);
|
||||
if (preset === '7d') { const f = new Date(now); f.setDate(f.getDate() - 6); return { from: toISO(f), to: today }; }
|
||||
if (preset === '30d') { const f = new Date(now); f.setDate(f.getDate() - 29); return { from: toISO(f), to: today }; }
|
||||
if (preset === '90d') { const f = new Date(now); f.setDate(f.getDate() - 89); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'thisMonth') { const f = new Date(now.getFullYear(), now.getMonth(), 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastMonth') { const f = new Date(now.getFullYear(), now.getMonth() - 1, 1); const t = new Date(now.getFullYear(), now.getMonth(), 0); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'year') { const f = new Date(now.getFullYear(), 0, 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastYear') { const f = new Date(now.getFullYear() - 1, 0, 1); const t = new Date(now.getFullYear() - 1, 11, 31); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'last12m') { const f = new Date(now); f.setFullYear(f.getFullYear() - 1); return { from: toISO(f), to: today }; }
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
function fmt(val: number, dec = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||
}
|
||||
function fmtB(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function KPICard({ label, value, sub, color = 'text-white', icon: Icon }: { label: string; value: string; sub?: string; color?: string; icon: any }) {
|
||||
return (
|
||||
<div className="relative bg-white/5 border border-white/10 rounded-2xl p-5 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]"><Icon className="w-14 h-14" /></div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-[#0f172a] border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1.5 min-w-[160px]">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white ml-auto">
|
||||
{typeof p.value === 'number' ? (p.value > 1000 ? fmtB(p.value) : fmt(p.value)) : p.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LazadaOverviewPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const loadData = (from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
fetch(`${API_URL}/api/lazada-order/analytics-overview${qs}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
};
|
||||
|
||||
useEffect(() => { loadData('', ''); }, []);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(dateFrom, dateTo);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
|
||||
<span className="text-slate-500">กำลังรวมข้อมูลทุกร้าน...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data || data.shops.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<Store className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน'}</p>
|
||||
<Link to="/lazada-order" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 text-sm">
|
||||
← กลับไปหน้า Lazada Orders
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { shops, monthly, topProducts, summary } = data;
|
||||
|
||||
// Pie data for revenue share
|
||||
const pieData = shops.map((s: any) => ({ name: shopLabel(s.shop), value: s.revenue, id: s.shop }));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-500/30 to-indigo-500/20 border border-white/10 flex items-center justify-center">
|
||||
<Store className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Lazada</p>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">ภาพรวมทุกร้าน</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<Link
|
||||
key={s.shop}
|
||||
to="/lazada-analytics"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl border text-xs font-bold transition-all hover:opacity-80"
|
||||
style={{ borderColor: `${shopHex(s.shop, i)}40`, backgroundColor: `${shopHex(s.shop, i)}15`, color: shopHex(s.shop, i) }}
|
||||
>
|
||||
{shopLabel(s.shop)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
|
||||
preset === p.key
|
||||
? 'bg-blue-500/15 text-blue-400 border-blue-500/30'
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-blue-500/40 focus:outline-none"
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-blue-500/40 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className="px-4 py-1.5 rounded-lg bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-medium hover:bg-blue-500/20 transition-all"
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active badge + clear */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData('', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grand KPI Row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<KPICard label="ยอดขายรวมทุกร้าน" value={fmtB(summary.totalRevenue)} sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} color="text-emerald-400" icon={DollarSign} />
|
||||
<KPICard label="รายได้สุทธิรวม (หักค่าส่ง)" value={fmtB(summary.netRevenue)} sub={`Margin ${fmt(summary.totalRevenue > 0 ? (summary.netRevenue / summary.totalRevenue) * 100 : 0, 1)}%`} color="text-cyan-glow" icon={TrendingUp} />
|
||||
<KPICard label="ส่วนลด Platform รวม" value={fmtB(summary.totalPlatformDisc)} sub="Lazada อุดหนุนส่วนลด" color="text-purple-400" icon={BadgePercent} />
|
||||
<KPICard label="AOV รวม" value={fmtB(summary.aov)} sub={`ยกเลิก ${fmt(summary.cancelRate, 1)}%`} color="text-amber-400" icon={ShoppingCart} />
|
||||
</div>
|
||||
|
||||
{/* Per-Shop Cards */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{shops.map((s: any, i: number) => {
|
||||
const hex = shopHex(s.shop, i);
|
||||
const revShare = summary.totalRevenue > 0 ? (s.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<div
|
||||
key={s.shop}
|
||||
className="bg-white/5 border border-white/10 rounded-2xl p-5 space-y-4"
|
||||
style={{ borderTopColor: hex, borderTopWidth: '2px' }}
|
||||
>
|
||||
{/* Shop header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-bold text-white text-base">{shopLabel(s.shop)}</p>
|
||||
<p className="text-[10px] font-mono text-slate-500">{s.shop}</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold px-2 py-1 rounded-lg" style={{ backgroundColor: `${hex}20`, color: hex }}>
|
||||
{fmt(revShare, 1)}% of total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
{[
|
||||
{ label: 'ยอดขาย', value: fmtB(s.revenue), color: 'text-emerald-400' },
|
||||
{ label: 'ส่วนลด Lazada', value: fmtB(s.platformDiscount), color: 'text-purple-400' },
|
||||
{ label: 'คำสั่งซื้อ', value: fmt(s.orders), color: 'text-white' },
|
||||
{ label: 'ส่วนลดร้าน', value: fmtB(s.sellerDiscount), color: 'text-rose-400' },
|
||||
{ label: 'ยกเลิก', value: `${fmt(s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0, 1)}%`, color: 'text-amber-400' },
|
||||
{ label: 'ค่าส่งรวม', value: fmtB(s.shippingFee), color: 'text-slate-300' },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
<p className="text-slate-500 text-[10px] uppercase tracking-wider">{item.label}</p>
|
||||
<p className={`font-bold font-mono ${item.color}`}>{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Revenue bar */}
|
||||
<div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all" style={{ width: `${Math.min(revShare, 100)}%`, backgroundColor: hex }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/lazada-analytics"
|
||||
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-xl text-xs font-medium border transition-all hover:opacity-90"
|
||||
style={{ borderColor: `${hex}30`, backgroundColor: `${hex}10`, color: hex }}
|
||||
>
|
||||
<TrendingUp className="w-3.5 h-3.5" /> ดู Analytics
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Monthly Stacked Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-5 h-5 text-blue-500" />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน (แยกตามร้าน)</h2>
|
||||
</div>
|
||||
{monthly.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthly} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barCategoryGap="30%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="month" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => v >= 1000 ? `฿${(v / 1000).toFixed(0)}k` : `฿${v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} formatter={(value) => shopLabel(value.replace('_revenue', ''))} />
|
||||
{shops.map((shop: any, i: number) => (
|
||||
<Bar key={shop.shop} dataKey={`${shop.shop}_revenue`} name={`${shop.shop}_revenue`} stackId="rev" fill={shopHex(shop.shop, i)} radius={i === shops.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]} />
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Revenue Share Pie + Per-shop comparison table */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<BadgePercent className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนยอดขายตามร้าน</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-[220px] flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={pieData} cx="50%" cy="50%" innerRadius={55} outerRadius={90} dataKey="value" nameKey="name" stroke="none" paddingAngle={3}>
|
||||
{pieData.map((entry: any, i: number) => (
|
||||
<Cell key={i} fill={shopHex(entry.id, i)} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }}
|
||||
formatter={(v: any) => fmtB(v)} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<div key={s.shop} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="text-slate-300 font-medium w-24">{shopLabel(s.shop)}</span>
|
||||
<span className="font-mono text-white">{fmtB(s.revenue)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Store className="w-5 h-5 text-slate-400" />
|
||||
<h2 className="text-lg font-bold text-white">เปรียบเทียบร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left">ร้าน</th>
|
||||
<th className="pb-3 text-right">ยอดขาย</th>
|
||||
<th className="pb-3 text-right">ส่วนลดแพลตฟอร์ม</th>
|
||||
<th className="pb-3 text-right">ออเดอร์</th>
|
||||
<th className="pb-3 text-right">ยกเลิก</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<tr key={s.shop} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="font-medium text-white">{shopLabel(s.shop)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-right font-mono text-emerald-400 font-bold">{fmtB(s.revenue)}</td>
|
||||
<td className="py-3 text-right font-mono text-purple-400">{fmtB(s.platformDiscount)}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300">{fmt(s.orders)}</td>
|
||||
<td className="py-3 text-right font-mono text-rose-400">{fmt(s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0, 1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Total row */}
|
||||
<tr className="bg-white/[0.03]">
|
||||
<td className="py-3 font-bold text-white text-xs">รวมทั้งหมด</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400">{fmtB(summary.totalRevenue)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-purple-400">{fmtB(summary.totalPlatformDisc)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-slate-300">{fmt(summary.totalOrders)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-rose-400">{fmt(summary.cancelRate, 1)}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products across all shops */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี — รวมทุกร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left w-8">#</th>
|
||||
<th className="pb-3 text-left">สินค้า</th>
|
||||
<th className="pb-3 text-left">ตัวเลือก</th>
|
||||
<th className="pb-3 text-right">ชิ้น</th>
|
||||
<th className="pb-3 text-right">ยอดรายได้</th>
|
||||
<th className="pb-3 text-right">% รวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3 text-slate-500 font-bold text-xs">{i + 1}</td>
|
||||
<td className="py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="py-3 text-slate-400 text-xs">{p.variation || '—'}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300 text-xs">{fmt(p.qty)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400 text-xs">{fmtB(p.revenue)}</td>
|
||||
<td className="py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-14 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-purple-400/60" style={{ width: `${Math.min(pct * 5, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-400 w-9 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Lock, User, AlertCircle, ArrowRight, Loader2 } from 'lucide-react'
|
||||
import { API_BASE } from '@irelax/types'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || 'Login failed')
|
||||
}
|
||||
|
||||
// Store token
|
||||
localStorage.setItem('irelax_admin_token', data.token)
|
||||
localStorage.setItem('irelax_admin_user', JSON.stringify(data.user))
|
||||
|
||||
// Redirect to dashboard
|
||||
navigate('/')
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 bg-night-950 relative overflow-hidden">
|
||||
{/* Background Glows */}
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-cyan-glow/5 blur-[120px] rounded-full -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-night-700/5 blur-[120px] rounded-full translate-y-1/2 -translate-x-1/2" />
|
||||
|
||||
<div className="w-full max-w-md space-y-8 animate-fade-in relative z-10">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-block px-4 py-1.5 rounded-full bg-cyan-glow/10 border border-cyan-glow/20 text-[10px] font-bold uppercase tracking-[0.3em] text-cyan-glow mb-4">
|
||||
Authorized Access Only
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tighter text-white">
|
||||
iRelax <span className="text-cyan-glow">Admin</span>
|
||||
</h1>
|
||||
<p className="text-slate-500 text-sm">Welcome back to the sensory command center.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="admin-card space-y-6 border-white/10 shadow-2xl">
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-sm">
|
||||
<AlertCircle className="w-5 h-5 shrink-0" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Username</label>
|
||||
<div className="relative group">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-cyan-glow transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 bg-night-950/50 border border-white/5 focus:border-cyan-glow/30 rounded-xl outline-none text-white transition-all"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Password</label>
|
||||
<div className="relative group">
|
||||
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-cyan-glow transition-colors" />
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 bg-night-950/50 border border-white/5 focus:border-cyan-glow/30 rounded-xl outline-none text-white transition-all"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary py-4 flex items-center justify-center gap-2 group transition-all"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
Login to Console
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
|
||||
© 2026 iRelax Aroma & Essential Oils
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { API_BASE, formatPrice, type Order } from '@irelax/types'
|
||||
import { ArrowLeft, CheckCircle2, ExternalLink, Loader2, Truck, XCircle, FileImage } from 'lucide-react'
|
||||
|
||||
export default function OrderDetailPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [order, setOrder] = useState<Order | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [updating, setUpdating] = useState(false)
|
||||
|
||||
const fetchOrder = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/orders/${id}`)
|
||||
const data = await res.json()
|
||||
setOrder(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrder()
|
||||
}, [id])
|
||||
|
||||
const updateStatus = async (status: string) => {
|
||||
setUpdating(true)
|
||||
try {
|
||||
await fetch(`${API_BASE}/orders/${id}/status`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status })
|
||||
})
|
||||
await fetchOrder()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to update status')
|
||||
} finally {
|
||||
setUpdating(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading || !order) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-20 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-12">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/orders" className="p-2 glass rounded-lg hover:bg-white/10 transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-mono">Order #{order.id.toString().padStart(4, '0')}</h1>
|
||||
<p className="text-slate-500 text-sm">{new Date(order.createdAt).toLocaleString()}</p>
|
||||
</div>
|
||||
<div className={`ml-auto px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-widest border ${
|
||||
order.status === 'PAID' ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' :
|
||||
order.status === 'SHIPPED' ? 'bg-cyan-glow/10 text-cyan-glow border-cyan-glow/20' :
|
||||
order.status === 'CANCELLED' ? 'bg-rose-500/10 text-rose-400 border-rose-500/20' :
|
||||
'bg-amber-500/10 text-amber-400 border-amber-500/20'
|
||||
}`}>
|
||||
{order.status.replace('_', ' ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Col: Order Info & Items */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
|
||||
<div className="admin-card p-0 overflow-hidden">
|
||||
<h2 className="p-6 border-b border-white/5 font-bold uppercase tracking-widest text-xs text-slate-400">Order Items</h2>
|
||||
<div className="divide-y divide-white/5">
|
||||
{order.items.map(item => (
|
||||
<div key={item.id} className="p-6 flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-lg bg-white/5 flex-shrink-0 overflow-hidden">
|
||||
<img src={item.productImage || '/images/oil_ozone.png'} alt="Product" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-bold">{item.productName}</p>
|
||||
<p className="text-xs text-slate-500 uppercase">{item.variantSize}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-cyan-glow font-bold">{formatPrice(item.priceAtPurchase)}</p>
|
||||
<p className="text-xs text-slate-500">Qty: {item.quantity}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="bg-night-950/50 p-6 border-t border-white/5 space-y-2 text-sm font-medium text-slate-300">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>{formatPrice(order.totalAmount - order.shippingFee)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Shipping</span>
|
||||
<span>{formatPrice(order.shippingFee)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-lg font-bold text-white pt-4 mt-4 border-t border-white/10">
|
||||
<span>Total</span>
|
||||
<span className="text-cyan-glow">{formatPrice(order.totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card p-6 space-y-6">
|
||||
<h2 className="font-bold uppercase tracking-widest text-xs text-slate-400">Customer Details</h2>
|
||||
|
||||
{order.customer && (
|
||||
<div className="flex items-center gap-4 mb-4 p-4 bg-white/5 rounded-xl border border-cyan-glow/20">
|
||||
<img src={order.customer.pictureUrl || `https://ui-avatars.com/api/?name=${order.customer.displayName}`} className="w-12 h-12 rounded-full border-2 border-cyan-glow/50" />
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-cyan-glow font-bold mb-1">LINE Customer</p>
|
||||
<p className="font-bold text-white text-lg">{order.customer.displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase text-slate-500 mb-1">Name</p>
|
||||
<p className="font-bold">{order.customerName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase text-slate-500 mb-1">Phone</p>
|
||||
<p className="font-bold">{order.customerPhone}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase text-slate-500 mb-1">Email</p>
|
||||
<p className="font-medium text-slate-300">{order.customerEmail}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase text-slate-500 mb-1">Shipping Address</p>
|
||||
<p className="font-medium text-slate-300 leading-relaxed">{order.shippingAddress}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right Col: Verification & Actions */}
|
||||
<div className="space-y-8">
|
||||
|
||||
<div className="admin-card p-6 space-y-6">
|
||||
<h2 className="font-bold uppercase tracking-widest text-xs text-slate-400">Payment Verification</h2>
|
||||
|
||||
{order.paymentSlipUrl ? (
|
||||
<div className="space-y-4">
|
||||
<a
|
||||
href={order.paymentSlipUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block relative rounded-xl overflow-hidden border border-white/10 group bg-night-950 min-h-[200px]"
|
||||
>
|
||||
<img src={order.paymentSlipUrl} alt="Slip" className="w-full object-cover group-hover:opacity-50 transition-opacity" />
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="glass px-4 py-2 rounded-full flex items-center gap-2 font-bold text-xs uppercase tracking-widest">
|
||||
<ExternalLink className="w-4 h-4" /> Open Original
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 bg-white/5 rounded-xl border border-dashed border-white/10 space-y-3">
|
||||
<FileImage className="w-10 h-10 text-slate-600 mx-auto" />
|
||||
<p className="text-slate-500 text-sm">No slip uploaded yet.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="admin-card p-6 space-y-6">
|
||||
<h2 className="font-bold uppercase tracking-widest text-xs text-slate-400">Actions</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{order.status === 'PENDING_PAYMENT' && (
|
||||
<button
|
||||
onClick={() => updateStatus('PAID')}
|
||||
disabled={updating}
|
||||
className="w-full py-3 bg-emerald-500 text-night-900 font-bold uppercase tracking-widest text-xs rounded-lg hover:bg-emerald-400 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" /> Verify Payment (Mark Paid)
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'PAID' && (
|
||||
<button
|
||||
onClick={() => updateStatus('SHIPPED')}
|
||||
disabled={updating}
|
||||
className="w-full py-3 bg-cyan-glow text-night-900 font-bold uppercase tracking-widest text-xs rounded-lg hover:shadow-[0_0_15px_rgba(102,252,241,0.3)] transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<Truck className="w-4 h-4" /> Mark as Shipped
|
||||
</button>
|
||||
)}
|
||||
{order.status !== 'CANCELLED' && order.status !== 'SHIPPED' && (
|
||||
<button
|
||||
onClick={() => updateStatus('CANCELLED')}
|
||||
disabled={updating}
|
||||
className="w-full py-3 bg-rose-500/10 text-rose-500 font-bold uppercase tracking-widest text-xs rounded-lg hover:bg-rose-500/20 transition-colors border border-rose-500/20 flex items-center justify-center gap-2 mt-4"
|
||||
>
|
||||
<XCircle className="w-4 h-4" /> Cancel Order
|
||||
</button>
|
||||
)}
|
||||
{order.status === 'SHIPPED' && (
|
||||
<div className="text-center p-4 bg-white/5 rounded-lg border border-white/5 text-slate-400 text-sm">
|
||||
Order complete.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { API_BASE, formatPrice, type Order } from '@irelax/types'
|
||||
import { ChevronRight, Clock, CheckCircle2, Truck, XCircle } from 'lucide-react'
|
||||
|
||||
export default function OrdersPage() {
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filter, setFilter] = useState<string>('ALL')
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/orders`)
|
||||
const data = await res.json()
|
||||
setOrders(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const filteredOrders = orders.filter(o => filter === 'ALL' || o.status === filter)
|
||||
|
||||
const StatusIcon = ({ status }: { status: string }) => {
|
||||
switch (status) {
|
||||
case 'PENDING_PAYMENT': return <Clock className="w-4 h-4 text-amber-500" />
|
||||
case 'PAID': return <CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
case 'SHIPPED': return <Truck className="w-4 h-4 text-cyan-glow" />
|
||||
case 'CANCELLED': return <XCircle className="w-4 h-4 text-rose-500" />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Orders Management</h1>
|
||||
<p className="text-slate-500 text-sm">Review incoming orders and verify payments.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex bg-white/5 border border-white/5 rounded-xl p-1 w-max">
|
||||
{['ALL', 'PENDING_PAYMENT', 'PAID', 'SHIPPED', 'CANCELLED'].map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-widest transition-all ${
|
||||
filter === f ? 'bg-white/10 text-cyan-glow' : 'text-slate-500 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{f.replace('_', ' ')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Orders Table */}
|
||||
<div className="admin-card overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 text-[10px] uppercase tracking-widest text-slate-500 bg-black/20">
|
||||
<th className="p-4 rounded-tl-xl">Order ID</th>
|
||||
<th className="p-4">Customer</th>
|
||||
<th className="p-4">Date</th>
|
||||
<th className="p-4">Total</th>
|
||||
<th className="p-4">Status</th>
|
||||
<th className="p-4 rounded-tr-xl">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i} className="animate-pulse">
|
||||
<td className="p-4"><div className="h-4 w-16 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-4 w-32 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-4 w-24 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-4 w-16 bg-white/5 rounded" /></td>
|
||||
<td className="p-4"><div className="h-6 w-24 bg-white/5 rounded-full" /></td>
|
||||
<td className="p-4"><div className="h-8 w-16 bg-white/5 rounded-lg" /></td>
|
||||
</tr>
|
||||
))
|
||||
) : filteredOrders.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="p-8 text-center text-slate-500 italic">No orders found.</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredOrders.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-white/5 transition-colors group">
|
||||
<td className="p-4 font-mono text-cyan-glow text-sm">
|
||||
#{order.id.toString().padStart(4, '0')}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
{order.customer ? (
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<img src={order.customer.pictureUrl || `https://ui-avatars.com/api/?name=${order.customer.displayName}`} className="w-5 h-5 rounded-full border border-cyan-glow/50" />
|
||||
<span className="text-[10px] font-bold text-cyan-glow tracking-widest uppercase truncate max-w-[120px]">LINE: {order.customer.displayName}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<p className="font-bold">{order.customerName}</p>
|
||||
<p className="text-xs text-slate-500">{order.customerEmail}</p>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-slate-300">
|
||||
{new Date(order.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="p-4 font-bold">
|
||||
{formatPrice(order.totalAmount)}
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon status={order.status} />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest">{order.status.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<Link
|
||||
to={`/orders/${order.id}`}
|
||||
className="flex items-center gap-1 text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-cyan-glow transition-colors"
|
||||
>
|
||||
Review
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Trash2,
|
||||
Plus,
|
||||
Package,
|
||||
Tag,
|
||||
Info,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Image as ImageIcon,
|
||||
ImagePlus,
|
||||
ExternalLink,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { API_BASE, getCategoryImage, formatPrice, type Category, type Product, type Variant, type ProductImage, type RawMaterial, type BomItem } from '@irelax/types'
|
||||
import ReactQuill from 'react-quill-new'
|
||||
import 'react-quill-new/dist/quill.snow.css'
|
||||
|
||||
export default function ProductEditPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [product, setProduct] = useState<Product | null>(null)
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [rawMaterials, setRawMaterials] = useState<RawMaterial[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
// Form State
|
||||
const [categoryId, setCategoryId] = useState<number>(0)
|
||||
const [description, setDescription] = useState('')
|
||||
const [details, setDetails] = useState('')
|
||||
const [variants, setVariants] = useState<Partial<Variant>[]>([])
|
||||
const [images, setImages] = useState<Partial<ProductImage>[]>([])
|
||||
|
||||
const quillRef = useRef<any>(null)
|
||||
|
||||
const fixUrl = (url: string) => {
|
||||
if (!url) return ''
|
||||
return url.replace('/public/uploads/', '/uploads/')
|
||||
}
|
||||
|
||||
const imageHandler = () => {
|
||||
const url = prompt('Please enter the Image URL: \n(Or leave blank and click OK to upload from your computer)')
|
||||
|
||||
// If they typed something and clicked OK
|
||||
if (url) {
|
||||
const quill = quillRef.current?.getEditor()
|
||||
if (quill) {
|
||||
const range = quill.getSelection(true)
|
||||
quill.insertEmbed(range?.index || 0, 'image', url)
|
||||
}
|
||||
}
|
||||
// If they left it blank and clicked OK
|
||||
else if (url === "") {
|
||||
const input = document.createElement('input')
|
||||
input.setAttribute('type', 'file')
|
||||
input.setAttribute('accept', 'image/*')
|
||||
input.click()
|
||||
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0]
|
||||
if (file) {
|
||||
// Use standard base64 embedding for normal file uploads
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const quill = quillRef.current?.getEditor()
|
||||
if (quill) {
|
||||
const range = quill.getSelection(true)
|
||||
quill.insertEmbed(range?.index || 0, 'image', e.target?.result)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modules = useMemo(() => ({
|
||||
toolbar: {
|
||||
container: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike', 'blockquote'],
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
],
|
||||
handlers: {
|
||||
image: imageHandler
|
||||
}
|
||||
}
|
||||
}), [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [prodRes, catRes, rawRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/products/${id}`),
|
||||
fetch(`${API_BASE}/categories`),
|
||||
fetch(`${API_BASE}/raw-materials`)
|
||||
])
|
||||
|
||||
if (!prodRes.ok) throw new Error('Product not found')
|
||||
|
||||
const prodData: Product = await prodRes.json()
|
||||
const catData: Category[] = await catRes.json()
|
||||
const rawData: RawMaterial[] = await rawRes.json()
|
||||
|
||||
setProduct(prodData)
|
||||
setCategories(catData)
|
||||
setRawMaterials(rawData)
|
||||
setCategoryId(prodData.categoryId)
|
||||
setDescription(prodData.description || '')
|
||||
setDetails(prodData.details || '')
|
||||
setVariants(prodData.variants || [])
|
||||
setImages(prodData.images || [])
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [id])
|
||||
|
||||
const handleAddVariant = () => {
|
||||
setVariants([...variants, { size: '', price: 0 }])
|
||||
}
|
||||
|
||||
const handleRemoveVariant = (index: number) => {
|
||||
setVariants(variants.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleUpdateVariant = (index: number, field: keyof Variant, value: any) => {
|
||||
const updated = [...variants]
|
||||
updated[index] = { ...updated[index], [field]: value }
|
||||
setVariants(updated)
|
||||
}
|
||||
|
||||
const handleAddBomItem = (variantIndex: number) => {
|
||||
if (rawMaterials.length === 0) return alert('No raw materials available')
|
||||
const updated = [...variants]
|
||||
const currentBom = updated[variantIndex].bomItems || []
|
||||
updated[variantIndex] = {
|
||||
...updated[variantIndex],
|
||||
bomItems: [...currentBom, { rawMaterialId: rawMaterials[0].id, quantity: 1 }]
|
||||
}
|
||||
setVariants(updated)
|
||||
}
|
||||
|
||||
const handleUpdateBomItem = (variantIndex: number, bomIndex: number, field: keyof BomItem, value: any) => {
|
||||
const updated = [...variants]
|
||||
const currentBom = [...(updated[variantIndex].bomItems || [])]
|
||||
currentBom[bomIndex] = { ...currentBom[bomIndex], [field]: value }
|
||||
updated[variantIndex].bomItems = currentBom
|
||||
setVariants(updated)
|
||||
}
|
||||
|
||||
const handleRemoveBomItem = (variantIndex: number, bomIndex: number) => {
|
||||
const updated = [...variants]
|
||||
const currentBom = [...(updated[variantIndex].bomItems || [])]
|
||||
currentBom.splice(bomIndex, 1)
|
||||
updated[variantIndex].bomItems = currentBom
|
||||
setVariants(updated)
|
||||
}
|
||||
|
||||
const handleAddImage = () => {
|
||||
setImages([...images, { url: '', order: images.length }])
|
||||
}
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
setImages(images.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleUpdateImage = (index: number, value: string) => {
|
||||
const updated = [...images]
|
||||
updated[index] = { ...updated[index], url: value }
|
||||
setImages(updated)
|
||||
}
|
||||
|
||||
const onFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
if (!res.ok) throw new Error('Upload failed')
|
||||
|
||||
const data = await res.json()
|
||||
// Add the new uploaded image to the list
|
||||
setImages([...images, { url: data.url, order: images.length }])
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
setSuccess(false)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/products/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
categoryId,
|
||||
description,
|
||||
details,
|
||||
variants: variants.map(v => ({
|
||||
size: v.size,
|
||||
price: Number(v.price),
|
||||
bomItems: v.bomItems?.map(b => ({
|
||||
rawMaterialId: Number(b.rawMaterialId),
|
||||
quantity: Number(b.quantity)
|
||||
})) || []
|
||||
})),
|
||||
images: images.filter(img => img.url).map((img, idx) => ({
|
||||
url: img.url,
|
||||
order: img.order ?? idx
|
||||
}))
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update product')
|
||||
|
||||
setSuccess(true)
|
||||
setTimeout(() => setSuccess(false), 3000)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="h-[60vh] flex flex-col items-center justify-center space-y-4">
|
||||
<Loader2 className="w-10 h-10 text-cyan-glow animate-spin" />
|
||||
<p className="text-slate-500 animate-pulse">Loading product details...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error && !product) {
|
||||
return (
|
||||
<div className="admin-card text-center py-20 space-y-4">
|
||||
<AlertCircle className="w-16 h-16 text-rose-500 mx-auto" />
|
||||
<h2 className="text-2xl font-bold">Error</h2>
|
||||
<p className="text-slate-500">{error}</p>
|
||||
<Link to="/products" className="btn-primary inline-flex items-center gap-2">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back to Products
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-8 animate-fade-in">
|
||||
{/* Header & Actions */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<Link to="/products" className="hover:text-cyan-glow transition-colors">Products</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white">Edit</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Edit <span className="text-cyan-glow">{product?.scent.name}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/products')}
|
||||
className="px-6 py-2.5 rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 text-sm font-bold transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-primary py-2.5 px-8 flex items-center gap-2"
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="flex items-center gap-3 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl text-emerald-500 text-sm animate-fade-in">
|
||||
<CheckCircle2 className="w-5 h-5 shrink-0" />
|
||||
<p>Product updated successfully!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-500 text-sm animate-fade-in">
|
||||
<AlertCircle className="w-5 h-5 shrink-0" />
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column: Identities */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Basic Info */}
|
||||
<section className="admin-card space-y-6">
|
||||
<div className="flex items-center gap-2 text-cyan-glow">
|
||||
<Info className="w-5 h-5" />
|
||||
<h2 className="font-bold text-lg">Basic Information</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Scent Name</label>
|
||||
<div className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-slate-400 text-sm italic">
|
||||
{product?.scent.name} (ReadOnly)
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600 px-1 mt-1">Scent properties are linked to the global library.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Category</label>
|
||||
<select
|
||||
value={categoryId}
|
||||
onChange={(e) => setCategoryId(Number(e.target.value))}
|
||||
className="w-full px-4 py-3 bg-night-950 border border-white/10 focus:border-cyan-glow/40 rounded-xl outline-none text-sm text-white transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
{categories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500 ml-1">Short Description</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-night-950 border border-white/10 focus:border-cyan-glow/40 rounded-xl outline-none text-sm text-white transition-all min-h-[80px]"
|
||||
placeholder="A brief summary of the product (shows under the title)."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 space-y-2 prose-editor-container bg-white text-black rounded-lg overflow-hidden">
|
||||
<div className="bg-night-950 text-slate-400 p-2 text-[10px] uppercase font-bold tracking-widest px-4 border-b border-white/10">Full Details</div>
|
||||
<ReactQuill
|
||||
ref={quillRef}
|
||||
theme="snow"
|
||||
value={details}
|
||||
onChange={setDetails}
|
||||
modules={modules}
|
||||
className="min-h-[300px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
.ql-editor { min-height: 300px; font-size: 16px; font-family: inherit; }
|
||||
`}</style>
|
||||
</section>
|
||||
|
||||
{/* Image Management */}
|
||||
<section className="admin-card space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-cyan-glow">
|
||||
<ImageIcon className="w-5 h-5" />
|
||||
<h2 className="font-bold text-lg">Images Gallery</h2>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => document.getElementById('file-upload')?.click()}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-slate-300 text-[10px] font-bold uppercase tracking-widest hover:bg-white/10 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Upload Local Image
|
||||
</button>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={onFileUpload}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddImage}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow text-[10px] font-bold uppercase tracking-widest hover:bg-cyan-glow/20 transition-all"
|
||||
>
|
||||
<ImagePlus className="w-3.5 h-3.5" />
|
||||
Add Image URL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{images.map((img, idx) => (
|
||||
<div key={idx} className="flex gap-4 p-4 bg-white/[0.02] border border-white/5 rounded-2xl group animate-fade-in items-center">
|
||||
<div className="w-16 h-16 rounded-lg overflow-hidden bg-night-950 border border-white/5 flex-shrink-0">
|
||||
{img.url ? (
|
||||
<img src={fixUrl(img.url)} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-slate-700">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-600 ml-1">Image {idx + 1} URL</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={img.url}
|
||||
onChange={(e) => handleUpdateImage(idx, e.target.value)}
|
||||
placeholder="https://..."
|
||||
className="w-full pl-4 pr-10 py-2.5 bg-night-950 border border-white/10 focus:border-cyan-glow/30 rounded-xl outline-none text-sm text-white transition-all shadow-inner"
|
||||
/>
|
||||
{img.url && (
|
||||
<a href={img.url} target="_blank" rel="noreferrer" className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-cyan-glow transition-colors">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRemoveImage(idx)}
|
||||
className="p-2.5 rounded-xl text-slate-600 hover:text-rose-500 hover:bg-rose-500/10 transition-all"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{images.length === 0 && (
|
||||
<div className="text-center py-12 border-2 border-dashed border-white/5 rounded-2xl space-y-3">
|
||||
<div className="w-12 h-12 bg-white/5 rounded-full flex items-center justify-center mx-auto text-slate-600">
|
||||
<ImageIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<p className="text-slate-600 italic text-sm">No product images added yet. Default category image will be used.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Variants Management */}
|
||||
<section className="admin-card space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-cyan-glow">
|
||||
<Package className="w-5 h-5" />
|
||||
<h2 className="font-bold text-lg">Pricing & Variants</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddVariant}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow text-[10px] font-bold uppercase tracking-widest hover:bg-cyan-glow/20 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Add Size
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{variants.map((v, idx) => (
|
||||
<div key={idx} className="p-4 bg-white/[0.02] border border-white/5 rounded-2xl group animate-fade-in space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-600 ml-1">Size / Volume</label>
|
||||
<input
|
||||
type="text"
|
||||
value={v.size}
|
||||
onChange={(e) => handleUpdateVariant(idx, 'size', e.target.value)}
|
||||
placeholder="e.g. 10ml, 50ml, Pack of 3"
|
||||
className="w-full px-4 py-2.5 bg-night-950 border border-white/10 focus:border-cyan-glow/30 rounded-xl outline-none text-sm text-white transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-600 ml-1">Price (THB)</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm font-bold">฿</span>
|
||||
<input
|
||||
type="number"
|
||||
value={v.price ?? ''}
|
||||
onChange={(e) => handleUpdateVariant(idx, 'price', e.target.value)}
|
||||
className="w-full pl-9 pr-4 py-2.5 bg-night-950 border border-white/10 focus:border-cyan-glow/30 rounded-xl outline-none text-sm text-white transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end pb-1 translate-y-1">
|
||||
<button
|
||||
onClick={() => handleRemoveVariant(idx)}
|
||||
className="p-2.5 rounded-xl text-slate-600 hover:text-rose-500 hover:bg-rose-500/10 transition-all"
|
||||
title="Remove Variant"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BOM Items for this Variant */}
|
||||
<div className="mt-4 pl-4 border-l-2 border-white/5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-wider">Bill of Materials (BOM)</p>
|
||||
<button
|
||||
onClick={() => handleAddBomItem(idx)}
|
||||
className="text-[10px] uppercase font-bold text-cyan-glow hover:text-white transition-colors"
|
||||
>
|
||||
+ Add Material
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{v.bomItems && v.bomItems.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{v.bomItems.map((bom, bIdx) => (
|
||||
<div key={bIdx} className="flex gap-2 items-center">
|
||||
<select
|
||||
value={bom.rawMaterialId}
|
||||
onChange={(e) => handleUpdateBomItem(idx, bIdx, 'rawMaterialId', Number(e.target.value))}
|
||||
className="flex-1 px-3 py-1.5 bg-night-950 border border-white/10 rounded-lg outline-none text-xs text-white"
|
||||
>
|
||||
{rawMaterials.map(rm => (
|
||||
<option key={rm.id} value={rm.id}>{rm.name} ({rm.unit})</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
value={bom.quantity}
|
||||
onChange={(e) => handleUpdateBomItem(idx, bIdx, 'quantity', Number(e.target.value))}
|
||||
placeholder="Qty"
|
||||
className="w-24 px-3 py-1.5 bg-night-950 border border-white/10 rounded-lg outline-none text-xs text-white"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveBomItem(idx, bIdx)}
|
||||
className="p-1.5 text-slate-500 hover:text-rose-500 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-slate-600 italic">No raw materials added. Cost will not be calculated.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{variants.length === 0 && (
|
||||
<div className="text-center py-10 border-2 border-dashed border-white/5 rounded-2xl">
|
||||
<p className="text-slate-600 italic text-sm">No variants added. A product must have at least one size variant.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Preview/Summary */}
|
||||
<div className="space-y-8">
|
||||
<section className="admin-card border-white/5 shadow-2xl overflow-hidden !p-0">
|
||||
<div className="p-6 bg-white/[0.02] border-b border-white/5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-cyan-glow">
|
||||
<Tag className="w-5 h-5" />
|
||||
<h2 className="font-bold text-lg">Preview Gallery</h2>
|
||||
</div>
|
||||
<div className="px-2 py-1 bg-white/5 rounded-md text-[10px] text-slate-500 font-bold uppercase tracking-widest">
|
||||
Live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Main Image Preview */}
|
||||
<div className="aspect-square rounded-2xl overflow-hidden bg-night-950 border border-white/5 p-4 flex items-center justify-center relative group">
|
||||
<img
|
||||
src={images.find(img => img.url)?.url ? fixUrl(images.find(img => img.url)!.url!) : getCategoryImage(product?.category.name || '')}
|
||||
className="w-full h-full object-contain transition-all duration-700"
|
||||
alt=""
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-950/80 to-transparent flex items-end p-6">
|
||||
<div className="space-y-1">
|
||||
<p className="text-cyan-glow font-bold text-xs uppercase tracking-widest">{product?.category.name}</p>
|
||||
<h3 className="text-xl font-bold text-white">{product?.scent.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Strip Preview */}
|
||||
{images.length > 0 && (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{images.filter(img => img.url).map((img, i) => (
|
||||
<div key={i} className="aspect-square rounded-lg overflow-hidden border border-white/10 bg-night-950">
|
||||
<img src={img.url} className={`w-full h-full object-cover ${i === 0 ? 'opacity-100' : 'opacity-40'}`} alt="" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 p-4 bg-white/5 rounded-2xl border border-white/5">
|
||||
<h4 className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 text-center">Summary</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Variants</span>
|
||||
<span className="text-white font-bold">{variants.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Min Price</span>
|
||||
<span className="text-cyan-glow font-bold">
|
||||
{variants.length > 0
|
||||
? formatPrice(Math.min(...variants.map(v => Number(v.price) || 0)))
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-slate-500">Images</span>
|
||||
<span className="text-white font-bold">{images.filter(img => img.url).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="p-6 rounded-2xl bg-cyan-glow/5 border border-cyan-glow/10 space-y-3">
|
||||
<p className="text-xs text-cyan-glow/70 leading-relaxed italic">
|
||||
Tip: The first image in the list will be used as the primary thumbnail across the store.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Package,
|
||||
Search,
|
||||
Plus,
|
||||
Eye,
|
||||
Edit3,
|
||||
Trash2,
|
||||
Filter,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Image as ImageIcon,
|
||||
X,
|
||||
UploadCloud,
|
||||
Loader2
|
||||
} from 'lucide-react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { API_BASE, getCategoryImage, formatPrice, type Category, type Product } from '@irelax/types'
|
||||
|
||||
interface PaginationMeta {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
lastPage: number;
|
||||
}
|
||||
|
||||
export default function ProductsPage() {
|
||||
const navigate = useNavigate()
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
const search = searchParams.get('search') || ''
|
||||
const activeCategory = searchParams.get('category') ? Number(searchParams.get('category')) : null
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
||||
const [isGalleryOpen, setIsGalleryOpen] = useState(false)
|
||||
|
||||
// Pagination State
|
||||
const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1
|
||||
const [limit] = useState(10) // Small limit for testing pagination UI easily
|
||||
const [meta, setMeta] = useState<PaginationMeta | null>(null)
|
||||
|
||||
const fixUrl = (url: string) => {
|
||||
if (!url) return ''
|
||||
// Fix legacy URLs that might have /public prefix which is now served at /
|
||||
return url.replace('/public/uploads/', '/uploads/')
|
||||
}
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
|
||||
const handleUploadFiles = async (files: FileList | File[]) => {
|
||||
if (!selectedProduct || files.length === 0) return
|
||||
|
||||
setIsUploading(true)
|
||||
try {
|
||||
const uploadPromises = Array.from(files).map(async (file) => {
|
||||
if (!file.type.startsWith('image/')) return null
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await fetch(`${API_BASE}/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
if (!res.ok) return null
|
||||
return await res.json()
|
||||
})
|
||||
|
||||
const results = await Promise.all(uploadPromises)
|
||||
const newImages = results.filter(r => r && r.success).map((r, idx) => ({
|
||||
url: r.url,
|
||||
order: (selectedProduct.images?.length || 0) + idx
|
||||
}))
|
||||
|
||||
if (newImages.length > 0) {
|
||||
// Update product images
|
||||
const updatedImages = [...(selectedProduct.images || []), ...newImages]
|
||||
const updateRes = await fetch(`${API_BASE}/products/${selectedProduct.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
categoryId: selectedProduct.categoryId,
|
||||
variants: selectedProduct.variants.map(v => ({ size: v.size, price: v.price })),
|
||||
images: updatedImages
|
||||
})
|
||||
})
|
||||
|
||||
if (updateRes.ok) {
|
||||
// Re-fetch all products to sync table state
|
||||
fetchData()
|
||||
|
||||
// Update selected product in modal
|
||||
const updatedProduct = { ...selectedProduct, images: updatedImages } as Product
|
||||
setSelectedProduct(updatedProduct)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
setIsDragging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
handleUploadFiles(e.dataTransfer.files)
|
||||
}
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
const items = e.clipboardData.items
|
||||
const files: File[] = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
const blob = items[i].getAsFile()
|
||||
if (blob) files.push(blob)
|
||||
}
|
||||
}
|
||||
if (files.length > 0) {
|
||||
handleUploadFiles(files)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteImage = async (imgUrl: string) => {
|
||||
if (!selectedProduct) return
|
||||
if (!confirm('Are you sure you want to delete this image?')) return
|
||||
|
||||
setIsUploading(true) // Reuse uploading state for delete overlay
|
||||
try {
|
||||
const updatedImages = (selectedProduct.images || []).filter(img => img.url !== imgUrl)
|
||||
const updateRes = await fetch(`${API_BASE}/products/${selectedProduct.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
categoryId: selectedProduct.categoryId,
|
||||
variants: selectedProduct.variants.map(v => ({ size: v.size, price: v.price })),
|
||||
images: updatedImages
|
||||
})
|
||||
})
|
||||
|
||||
if (updateRes.ok) {
|
||||
fetchData()
|
||||
const updatedProduct = { ...selectedProduct, images: updatedImages } as Product
|
||||
setSelectedProduct(updatedProduct)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err)
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const catPath = `${API_BASE}/categories`
|
||||
const prodPath = `${API_BASE}/products?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}${activeCategory ? `&categoryId=${activeCategory}` : ''}`
|
||||
|
||||
const [catRes, prodRes] = await Promise.all([
|
||||
fetch(catPath),
|
||||
fetch(prodPath)
|
||||
])
|
||||
|
||||
setCategories(await catRes.json())
|
||||
const prodData = await prodRes.json()
|
||||
setProducts(prodData.data || [])
|
||||
setMeta(prodData.meta || null)
|
||||
} catch (err) {
|
||||
console.error('Products fetch error:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const setPage = (newOrUpdaterData: number | ((p: number) => number)) => {
|
||||
const nextUrlPage = typeof newOrUpdaterData === 'function' ? newOrUpdaterData(page) : newOrUpdaterData
|
||||
setSearchParams(prev => {
|
||||
if (nextUrlPage <= 1) {
|
||||
prev.delete('page')
|
||||
} else {
|
||||
prev.set('page', nextUrlPage.toString())
|
||||
}
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch when page, category, or search changes
|
||||
useEffect(() => {
|
||||
// Only debounce if we have a search value to avoid rapid fetches while typing
|
||||
const timeout = setTimeout(() => {
|
||||
fetchData()
|
||||
}, search ? 300 : 0)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [page, activeCategory, search])
|
||||
|
||||
// Reset to page 1 when filter changes
|
||||
const handleCategoryChange = (catId: number | null) => {
|
||||
setSearchParams(prev => {
|
||||
if (catId) prev.set('category', catId.toString())
|
||||
else prev.delete('category')
|
||||
prev.delete('page')
|
||||
return prev
|
||||
}, { replace: true })
|
||||
}
|
||||
|
||||
const handleSearchChange = (val: string) => {
|
||||
setSearchParams(prev => {
|
||||
if (val) prev.set('search', val)
|
||||
else prev.delete('search')
|
||||
prev.delete('page')
|
||||
return prev
|
||||
}, { replace: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Products Management</h1>
|
||||
<p className="text-slate-500 text-sm">
|
||||
{meta ? `Displaying ${products.length} of ${meta.total} products` : 'Loading products...'}
|
||||
</p>
|
||||
</div>
|
||||
<button className="btn-primary flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Product
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters Bar */}
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="flex-1 w-full relative group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-cyan-glow transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search products..."
|
||||
value={search}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 bg-night-900/50 border border-white/5 focus:border-cyan-glow/30 rounded-xl outline-none text-sm placeholder:text-slate-600 transition-all shadow-inner text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<div className="p-3 bg-night-900 border border-white/5 rounded-xl">
|
||||
<Filter className="w-4 h-4 text-slate-500" />
|
||||
</div>
|
||||
<select
|
||||
value={activeCategory || ''}
|
||||
onChange={(e) => handleCategoryChange(Number(e.target.value) || null)}
|
||||
className="flex-1 md:w-52 px-4 py-3 bg-night-900 border border-white/5 focus:border-cyan-glow/30 rounded-xl outline-none text-sm text-silver-metallic"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products Table */}
|
||||
<div className="admin-card !p-0 overflow-hidden border-white/10 shadow-2xl">
|
||||
<div className="overflow-x-auto min-h-[400px]">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-white/[0.02] border-b border-white/5">
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Product</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Category</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Variants</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Base Price</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.03]">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i}>
|
||||
<td colSpan={5} className="px-6 py-8">
|
||||
<div className="h-10 w-full bg-white/5 animate-pulse rounded-lg" />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : products.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-20 text-center space-y-3">
|
||||
<Package className="w-10 h-10 text-slate-700 mx-auto" />
|
||||
<p className="text-slate-500 font-medium italic">No products matched your search.</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : products.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-white/[0.01] transition-colors group">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden border border-white/5 bg-night-950/50 p-1 flex-shrink-0 flex items-center justify-center">
|
||||
<img
|
||||
src={product.images && product.images.length > 0
|
||||
? fixUrl(product.images[0].url)
|
||||
: getCategoryImage(product.category.name)}
|
||||
alt=""
|
||||
className="w-full h-full object-contain grayscale-0 opacity-100 group-hover:scale-110 transition-all duration-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-sm font-bold text-white truncate">{product.scent.name}</p>
|
||||
<p className="text-[10px] uppercase font-bold text-slate-600 tracking-wider">PID: {product.id.toString().padStart(4, '0')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-block px-2.5 py-1 rounded-md bg-cyan-glow/10 text-cyan-glow text-[10px] font-bold uppercase tracking-wider border border-cyan-glow/20">
|
||||
{product.category.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-xs font-medium text-silver-metallic">{product.variants?.length || 0} Sizes</p>
|
||||
<p className="text-[10px] text-slate-600 truncate max-w-[120px]">
|
||||
{product.variants?.map(v => v.size).join(', ') || '-'}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm font-bold text-white">{formatPrice(product.price)}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => { setSelectedProduct(product); setIsGalleryOpen(true); }}
|
||||
className={`p-2 rounded-lg transition-all ${
|
||||
product.images && product.images.length > 0
|
||||
? 'bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow/20'
|
||||
: 'hover:bg-white/5 text-slate-600'
|
||||
}`}
|
||||
title={product.images && product.images.length > 0 ? "View Gallery" : "No Images"}
|
||||
>
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-all">
|
||||
<Eye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate(`/products/${product.id}/edit`)}
|
||||
className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-cyan-glow transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-rose-500 transition-all">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
<div className="px-6 py-4 bg-white/[0.01] border-t border-white/5 flex items-center justify-between">
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-xs text-slate-500">
|
||||
Showing page <span className="text-white font-bold">{page}</span> of {meta?.lastPage || 1}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page <= 1 || loading}
|
||||
className="p-2 rounded-lg bg-night-950 border border-white/5 disabled:opacity-30 disabled:cursor-not-allowed hover:border-white/10 transition-all text-white"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Page numbers (limited) */}
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, meta?.lastPage || 0) }).map((_, i) => {
|
||||
const pageNum = i + 1;
|
||||
// Logic for showing a realistic range could go here, but 5 is enough for now
|
||||
return (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => setPage(pageNum)}
|
||||
className={`w-8 h-8 rounded-lg text-xs font-bold transition-all ${
|
||||
page === pageNum
|
||||
? 'bg-cyan-glow text-night-900 border border-cyan-glow shadow-[0_0_10px_rgba(102,252,241,0.2)]'
|
||||
: 'hover:bg-white/5 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{(meta?.lastPage || 0) > 5 && <span className="text-slate-600 px-1">...</span>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(meta?.lastPage || p, p + 1))}
|
||||
disabled={page >= (meta?.lastPage || 1) || loading}
|
||||
className="p-2 rounded-lg bg-night-950 border border-white/5 disabled:opacity-30 disabled:cursor-not-allowed hover:border-white/10 transition-all text-white"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gallery Modal */}
|
||||
{isGalleryOpen && selectedProduct && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-night-950/80 backdrop-blur-sm" onClick={() => setIsGalleryOpen(false)} />
|
||||
<div
|
||||
className="relative w-full max-w-4xl max-h-[90vh] bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-zoom-in"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onPaste={handlePaste}
|
||||
>
|
||||
{/* Drag Overlay */}
|
||||
{isDragging && (
|
||||
<div className="absolute inset-0 z-50 bg-cyan-glow/20 backdrop-blur-md flex flex-col items-center justify-center border-2 border-dashed border-cyan-glow animate-pulse">
|
||||
<UploadCloud className="w-20 h-20 text-cyan-glow mb-4" />
|
||||
<p className="text-2xl font-bold text-white uppercase tracking-widest">Drop images here</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Uploading Overlay */}
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 z-50 bg-night-950/60 backdrop-blur-sm flex flex-col items-center justify-center">
|
||||
<Loader2 className="w-12 h-12 text-cyan-glow animate-spin mb-4" />
|
||||
<p className="text-xl font-bold text-white">Uploading assets...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">{selectedProduct.scent.name}</h3>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">{selectedProduct.category.name} Gallery</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsGalleryOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-500 hover:text-white transition-all"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 scrollbar-hide">
|
||||
{selectedProduct.images && selectedProduct.images.length > 0 ? (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{selectedProduct.images.map((img, i) => (
|
||||
<div key={i} className="group relative aspect-[4/5] rounded-2xl overflow-hidden border border-white/5 bg-night-950 p-2 shadow-xl hover:border-cyan-glow/30 transition-all">
|
||||
<img
|
||||
src={fixUrl(img.url)}
|
||||
alt=""
|
||||
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute top-4 right-4 flex gap-2">
|
||||
<div className="px-2 py-1 bg-night-950/80 backdrop-blur-md rounded-md text-[8px] font-bold text-slate-500 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
ORD #{img.order ?? i}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteImage(img.url); }}
|
||||
className="p-1.5 bg-rose-500/10 hover:bg-rose-500 backdrop-blur-md border border-rose-500/20 rounded-lg text-rose-500 hover:text-white opacity-0 group-hover:opacity-100 transition-all duration-300 transform scale-90 group-hover:scale-100"
|
||||
title="Delete Image"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[40vh] flex flex-col items-center justify-center space-y-6">
|
||||
<div className="relative">
|
||||
<ImageIcon className="w-20 h-20 text-slate-800" />
|
||||
<div className="absolute inset-0 bg-cyan-glow/5 animate-pulse blur-2xl rounded-full" />
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-2xl font-bold text-white">No custom images found</p>
|
||||
<p className="text-slate-500 max-w-sm mx-auto">
|
||||
This product currently relies on the category default placeholder. Add custom photos to enhance the customer experience.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-40 h-40 rounded-3xl overflow-hidden border border-white/5 p-6 bg-night-950/50 backdrop-blur-xl group">
|
||||
<img
|
||||
src={getCategoryImage(selectedProduct.category.name)}
|
||||
alt=""
|
||||
className="w-full h-full object-contain grayscale opacity-40 group-hover:grayscale-0 group-hover:opacity-100 transition-all duration-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 border-t border-white/5 bg-white/[0.01] flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 text-slate-500 whitespace-nowrap overflow-hidden">
|
||||
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
|
||||
<UploadCloud className="w-4 h-4" />
|
||||
</div>
|
||||
<p className="text-[10px] uppercase font-bold tracking-widest truncate">
|
||||
Drag & Drop or <span className="text-cyan-glow">Paste images</span> to upload directly
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(`/products/${selectedProduct.id}/edit`)}
|
||||
className="btn-primary px-6 py-2 w-full sm:w-auto"
|
||||
>
|
||||
Manage All Images
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Plus, Edit3, Trash2, Loader2, PackageOpen } from 'lucide-react'
|
||||
import { API_BASE, type RawMaterial } from '@irelax/types'
|
||||
|
||||
export default function RawMaterialsPage() {
|
||||
const [materials, setMaterials] = useState<RawMaterial[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [editingId, setEditingId] = useState<number | null>(null)
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [unit, setUnit] = useState('')
|
||||
const [pricePerUnit, setPricePerUnit] = useState<number>(0)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const fetchMaterials = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/raw-materials`)
|
||||
if (res.ok) {
|
||||
setMaterials(await res.json())
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch raw materials', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchMaterials()
|
||||
}, [])
|
||||
|
||||
const handleOpenModal = (material?: RawMaterial) => {
|
||||
if (material) {
|
||||
setEditingId(material.id)
|
||||
setName(material.name)
|
||||
setUnit(material.unit)
|
||||
setPricePerUnit(material.pricePerUnit)
|
||||
} else {
|
||||
setEditingId(null)
|
||||
setName('')
|
||||
setUnit('')
|
||||
setPricePerUnit(0)
|
||||
}
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name || !unit || pricePerUnit <= 0) return alert('Please fill all fields')
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const payload = { name, unit, pricePerUnit }
|
||||
const url = editingId
|
||||
? `${API_BASE}/raw-materials/${editingId}`
|
||||
: `${API_BASE}/raw-materials`
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: editingId ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setIsModalOpen(false)
|
||||
fetchMaterials()
|
||||
} else {
|
||||
alert('Failed to save raw material')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Are you sure you want to delete this raw material?')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/raw-materials/${id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
fetchMaterials()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Raw Materials (วัตถุดิบ)</h1>
|
||||
<p className="text-slate-500 text-sm">Manage inventory and costs for BOM calculation.</p>
|
||||
</div>
|
||||
<button onClick={() => handleOpenModal()} className="btn-primary flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Material
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="admin-card !p-0 overflow-hidden border-white/10 shadow-2xl">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-white/[0.02] border-b border-white/5">
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Material Name</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Unit</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">Cost Per Unit</th>
|
||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/[0.03]">
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<tr key={i}>
|
||||
<td colSpan={4} className="px-6 py-8">
|
||||
<div className="h-6 w-full bg-white/5 animate-pulse rounded-lg" />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : materials.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-20 text-center space-y-3">
|
||||
<PackageOpen className="w-10 h-10 text-slate-700 mx-auto" />
|
||||
<p className="text-slate-500 font-medium italic">No raw materials found.</p>
|
||||
</td>
|
||||
</tr>
|
||||
) : materials.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-white/[0.01] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm font-bold text-white">{item.name}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="inline-block px-2 py-1 rounded-md bg-night-950 border border-white/5 text-slate-400 text-[10px] uppercase font-bold tracking-wider">
|
||||
{item.unit}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm font-bold text-emerald-400">฿{item.pricePerUnit.toLocaleString(undefined, { minimumFractionDigits: 2 })}</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button onClick={() => handleOpenModal(item)} className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-cyan-glow transition-all">
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(item.id)} className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-rose-500 transition-all">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-night-950/80 backdrop-blur-sm" onClick={() => !saving && setIsModalOpen(false)} />
|
||||
<div className="relative w-full max-w-md bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden animate-zoom-in p-6 space-y-6">
|
||||
<h3 className="text-xl font-bold">{editingId ? 'Edit Material' : 'Add New Material'}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Material Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. Alcohol, Glass Bottle 50ml"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Unit</label>
|
||||
<input
|
||||
type="text"
|
||||
value={unit}
|
||||
onChange={e => setUnit(e.target.value)}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. kg, liter, piece"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Price per Unit (฿)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={pricePerUnit || ''}
|
||||
onChange={e => setPricePerUnit(Number(e.target.value))}
|
||||
className="admin-input w-full"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-white/5">
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 rounded-xl text-slate-400 hover:text-white hover:bg-white/5 font-medium transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-primary px-6 py-2 flex items-center gap-2"
|
||||
>
|
||||
{saving && <Loader2 className="w-4 h-4 animate-spin" />}
|
||||
{saving ? 'Saving...' : 'Save Material'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, LineChart, Line, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, ShoppingBag, DollarSign, ReceiptText, Package,
|
||||
ArrowLeft, BadgePercent, ShoppingCart, XCircle, CheckCircle2, Loader2,
|
||||
Calendar, Store,
|
||||
} from 'lucide-react';
|
||||
import { SHOPEE_SHOPS } from './ShopeePage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
function fmt(val: number, decimals = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(val);
|
||||
}
|
||||
function fmtCurrency(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function toISODate(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISODate(now);
|
||||
if (preset === '7d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 6);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '30d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 29);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '90d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 89);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'thisMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const to = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'year') {
|
||||
const from = new Date(now.getFullYear(), 0, 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastYear') {
|
||||
const from = new Date(now.getFullYear() - 1, 0, 1);
|
||||
const to = new Date(now.getFullYear() - 1, 11, 31);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'last12m') {
|
||||
const from = new Date(now); from.setFullYear(from.getFullYear() - 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
|
||||
|
||||
function StatCard({
|
||||
label, value, sub, icon: Icon, color = 'text-white', bgColor = 'from-white/5',
|
||||
}: { label: string; value: string | number; sub?: string; icon: any; color?: string; bgColor?: string }) {
|
||||
return (
|
||||
<div className={`relative bg-gradient-to-br ${bgColor} to-transparent border border-white/10 rounded-2xl p-5 overflow-hidden`}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]">
|
||||
<Icon className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-night-900 border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white">{typeof p.value === 'number' && p.name?.includes('฿') ? fmtCurrency(p.value) : fmt(p.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ShopeeAnalyticsPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Shop selector
|
||||
const [shop, setShop] = useState('hom-aroma');
|
||||
const shopCfg = SHOPEE_SHOPS.find(s => s.id === shop) ?? SHOPEE_SHOPS[0];
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const buildUrl = useCallback((s: string, from: string, to: string) => {
|
||||
const params = new URLSearchParams({ shop: s });
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
return `${API_URL}/api/shopee/analytics?${params}`;
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback((s: string, from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
fetch(buildUrl(s, from, to), { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
}, [buildUrl]);
|
||||
|
||||
// Reload on shop or initial mount
|
||||
useEffect(() => { loadData(shop, dateFrom, dateTo); }, [shop]);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(shop, range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(shop, dateFrom, dateTo);
|
||||
};
|
||||
|
||||
const handleShopChange = (s: string) => {
|
||||
setShop(s);
|
||||
// reset dates so we don't carry over filter from previous shop
|
||||
setPreset('all'); setDateFrom(''); setDateTo('');
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-orange-400 animate-spin" />
|
||||
<span className="text-slate-500">กำลังคำนวณข้อมูลวิเคราะห์...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<ShoppingBag className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ไม่มีข้อมูล — กรุณา Import ไฟล์ Shopee ก่อน'}</p>
|
||||
<Link to="/shopee" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 hover:text-white text-sm">
|
||||
<ArrowLeft className="w-4 h-4" /> กลับหน้า Shopee
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, monthly, statusBreakdown, paymentBreakdown, topProducts } = data;
|
||||
|
||||
// Prepare monthly chart (revenue + net bars, orders line)
|
||||
const monthlyChart = monthly.map((m: any) => ({
|
||||
...m,
|
||||
net: m.revenue - m.commission,
|
||||
label: m.month,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/shopee" className="p-2 rounded-xl border border-white/10 hover:bg-white/5 text-slate-400 hover:text-white transition-all">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<Link to="/shopee" className="hover:text-orange-400 transition-colors">Shopee</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white">Analytics</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
|
||||
<TrendingUp className={`w-6 h-6 ${shopCfg.color}`} />
|
||||
วิเคราะห์ยอดขาย —
|
||||
<span className={shopCfg.color}>{shopCfg.label}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shop Switcher */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Store className="w-4 h-4 text-slate-500" />
|
||||
{SHOPEE_SHOPS.map(s => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => handleShopChange(s.id)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold border transition-all ${
|
||||
shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
preset === p.key
|
||||
? 'bg-orange-500/20 text-orange-400 border border-orange-500/30'
|
||||
: 'bg-white/5 text-slate-400 border border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={e => setDateFrom(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-orange-500/50 focus:outline-none"
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={e => setDateTo(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-orange-500/50 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className="px-4 py-1.5 rounded-lg bg-orange-500/15 border border-orange-500/30 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-all"
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active filter badge */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData(shop, '', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* KPI Cards Row 1 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="ยอดขายรวม (Gross)" value={fmtCurrency(summary.totalRevenue)} icon={DollarSign} color="text-emerald-400" bgColor="from-emerald-500/10" sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} />
|
||||
<StatCard label="รายได้สุทธิ (Net)" value={fmtCurrency(summary.netRevenue)} icon={TrendingUp} color="text-cyan-glow" bgColor="from-cyan-500/10" sub={`หลังหัก Commission + ส่งออก`} />
|
||||
<StatCard label="ค่าคอมมิชชั่นรวม" value={fmtCurrency(summary.totalCommission)} icon={ReceiptText} color="text-rose-400" bgColor="from-rose-500/10" sub={`${fmt(summary.commissionRate, 1)}% ของยอดขาย`} />
|
||||
<StatCard label="AOV (เฉลี่ยต่อออเดอร์)" value={fmtCurrency(summary.aov)} icon={ShoppingCart} color="text-amber-400" bgColor="from-amber-500/10" sub="Average Order Value" />
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Row 2 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="คำสั่งซื้อสำเร็จ" value={fmt(summary.completedOrders)} icon={CheckCircle2} color="text-emerald-400" sub={`จากทั้งหมด ${fmt(summary.totalOrders)}`} />
|
||||
<StatCard label="คำสั่งซื้อยกเลิก" value={fmt(summary.cancelledOrders)} icon={XCircle} color="text-rose-400" sub={`${fmt(summary.cancelRate, 1)}% ของทั้งหมด`} />
|
||||
<StatCard label="ค่าจัดส่ง (ผู้ซื้อ)" value={fmtCurrency(summary.totalShipping)} icon={Package} color="text-blue-400" sub="ค่าส่งที่ผู้ซื้อจ่าย" />
|
||||
<StatCard label="Effective Net Margin" value={`${fmt(summary.totalRevenue > 0 ? (summary.netRevenue / summary.totalRevenue) * 100 : 0, 1)}%`} icon={BadgePercent} color="text-purple-400" sub="Net / Gross Revenue" />
|
||||
</div>
|
||||
|
||||
{/* Monthly Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-5 h-5 text-orange-400" />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน</h2>
|
||||
<span className="text-xs text-slate-500 ml-2">Gross Revenue vs Net Revenue</span>
|
||||
</div>
|
||||
{monthlyChart.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barGap={3}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => `฿${v >= 1000 ? `${(v / 1000).toFixed(0)}k` : v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} />
|
||||
<Bar dataKey="revenue" name="฿ ยอดขาย (Gross)" fill="#4ade80" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="net" name="฿ รายได้สุทธิ (Net)" fill="#22d3ee" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="commission" name="฿ ค่าคอม" fill="#f87171" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Orders per month line chart */}
|
||||
{monthlyChart.length > 1 && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ShoppingBag className="w-5 h-5 text-cyan-glow" />
|
||||
<h2 className="text-lg font-bold text-white">จำนวนคำสั่งซื้อรายเดือน</h2>
|
||||
</div>
|
||||
<div className="h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line type="monotone" dataKey="orders" name="คำสั่งซื้อ" stroke="#f97316" strokeWidth={2} dot={{ r: 4, fill: '#f97316' }} />
|
||||
<Line type="monotone" dataKey="items" name="จำนวนสินค้า (ชิ้น)" stroke="#818cf8" strokeWidth={2} dot={{ r: 4, fill: '#818cf8' }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status + Payment row */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Status Breakdown */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนสถานะคำสั่งซื้อ</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-center">
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={statusBreakdown} cx="50%" cy="50%" innerRadius={50} outerRadius={80} dataKey="count" nameKey="status" stroke="none" paddingAngle={2}>
|
||||
{statusBreakdown.map((_: any, i: number) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{statusBreakdown.map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: COLORS[i % COLORS.length] }} />
|
||||
<span className="text-slate-300 flex-1 truncate" title={s.status}>{s.status}</span>
|
||||
<span className="font-bold text-white">{s.count}</span>
|
||||
<span className="text-slate-500">({fmt((s.count / summary.totalOrders) * 100, 0)}%)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<DollarSign className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">ช่องทางการชำระเงิน</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{paymentBreakdown.map((p: any, i: number) => {
|
||||
const pct = summary.totalOrders > 0 ? (p.count / summary.totalOrders) * 100 : 0;
|
||||
return (
|
||||
<div key={i} className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-300 truncate max-w-[160px]" title={p.method}>{p.method}</span>
|
||||
<span className="text-slate-400 ml-2">{p.count} ออเดอร์ · {fmt(pct, 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{ width: `${pct}%`, backgroundColor: COLORS[i % COLORS.length] }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี (by ยอดเงิน)</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/[0.03] border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3">#</th>
|
||||
<th className="px-4 py-3">สินค้า</th>
|
||||
<th className="px-4 py-3">ตัวเลือก</th>
|
||||
<th className="px-4 py-3 text-right">ชิ้น</th>
|
||||
<th className="px-4 py-3 text-right">ออเดอร์</th>
|
||||
<th className="px-4 py-3 text-right">ยอดรายได้</th>
|
||||
<th className="px-4 py-3 text-right">% จากยอดรวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<span className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-xs font-bold text-slate-400">
|
||||
{i + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{p.variant || '—'}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-slate-300">{fmt(p.qty)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-slate-300">{fmt(p.orders)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">{fmtCurrency(p.revenue)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-16 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-emerald-400/60" style={{ width: `${Math.min(pct * 3, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cost Waterfall Summary */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ReceiptText className="w-5 h-5 text-rose-400" />
|
||||
<h2 className="text-lg font-bold text-white">สรุปค่าใช้จ่าย (Cost Breakdown)</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{[
|
||||
{ label: 'ยอดขาย Gross', value: summary.totalRevenue, color: 'text-emerald-400', bar: 'bg-emerald-400' },
|
||||
{ label: 'หัก: ค่าคอมมิชชั่น', value: -summary.totalCommission, color: 'text-rose-400', bar: 'bg-rose-400' },
|
||||
{ label: 'หัก: ค่าจัดส่ง (ผู้ซื้อ)', value: -summary.totalShipping, color: 'text-orange-400', bar: 'bg-orange-400' },
|
||||
{ label: '= รายได้สุทธิ', value: summary.netRevenue, color: 'text-cyan-glow', bar: 'bg-cyan-glow', highlight: true },
|
||||
].map((item, i) => (
|
||||
<div key={i} className={`p-4 rounded-xl border ${item.highlight ? 'border-cyan-glow/20 bg-cyan-glow/5' : 'border-white/10 bg-white/[0.02]'}`}>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{item.label}</p>
|
||||
<p className={`text-xl font-bold font-mono ${item.color}`}>{fmtCurrency(Math.abs(item.value))}</p>
|
||||
{summary.totalRevenue > 0 && (
|
||||
<p className="text-[10px] text-slate-600 mt-1">{fmt(Math.abs(item.value / summary.totalRevenue) * 100, 1)}% ของยอด</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-white/5 overflow-hidden flex">
|
||||
{summary.totalRevenue > 0 && (
|
||||
<>
|
||||
<div className="h-full bg-rose-400/70 transition-all" style={{ width: `${(summary.totalCommission / summary.totalRevenue) * 100}%` }} title={`Commission: ${fmt((summary.totalCommission / summary.totalRevenue) * 100, 1)}%`} />
|
||||
<div className="h-full bg-orange-400/70 transition-all" style={{ width: `${(summary.totalShipping / summary.totalRevenue) * 100}%` }} title={`Shipping: ${fmt((summary.totalShipping / summary.totalRevenue) * 100, 1)}%`} />
|
||||
<div className="h-full bg-cyan-glow/60 transition-all" style={{ width: `${(summary.netRevenue / summary.totalRevenue) * 100}%` }} title={`Net: ${fmt((summary.netRevenue / summary.totalRevenue) * 100, 1)}%`} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-6 mt-3 text-[10px] text-slate-500">
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-rose-400/70" /> Commission ({fmt(summary.commissionRate, 1)}%)</span>
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-orange-400/70" /> ค่าจัดส่ง ({fmt(summary.totalRevenue > 0 ? (summary.totalShipping / summary.totalRevenue) * 100 : 0, 1)}%)</span>
|
||||
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-cyan-glow/60" /> Net ({fmt(summary.totalRevenue > 0 ? (summary.netRevenue / summary.totalRevenue) * 100 : 0, 1)}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ShoppingBag,
|
||||
Package,
|
||||
Truck,
|
||||
User,
|
||||
MapPin,
|
||||
CreditCard,
|
||||
Receipt,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
Hash,
|
||||
Tag,
|
||||
BadgePercent,
|
||||
} from 'lucide-react'
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '')
|
||||
|
||||
function authHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}`,
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(val: number | null | undefined) {
|
||||
if (val == null) return '—'
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val)
|
||||
}
|
||||
|
||||
function InfoRow({ label, value, mono }: { label: string; value?: string | null; mono?: boolean }) {
|
||||
if (!value) return null
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<p className="text-[10px] uppercase font-bold tracking-widest text-slate-500">{label}</p>
|
||||
<p className={`text-sm text-slate-200 leading-relaxed ${mono ? 'font-mono' : ''}`}>{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function statusStyle(status: string) {
|
||||
if (status.includes('สำเร็จ')) return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20'
|
||||
if (status.includes('ได้รับสินค้า')) return 'bg-blue-500/15 text-blue-400 border-blue-500/20'
|
||||
if (status.includes('ยกเลิก')) return 'bg-rose-500/15 text-rose-400 border-rose-500/20'
|
||||
return 'bg-amber-500/15 text-amber-400 border-amber-500/20'
|
||||
}
|
||||
|
||||
export default function ShopeeOrderDetailPage() {
|
||||
const { orderId } = useParams<{ orderId: string }>()
|
||||
const [order, setOrder] = useState<any | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!orderId) return
|
||||
setLoading(true)
|
||||
fetch(`${API_URL}/api/shopee/orders/${encodeURIComponent(orderId)}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) throw new Error(data.error)
|
||||
setOrder(data)
|
||||
})
|
||||
.catch(err => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [orderId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4 animate-pulse">
|
||||
<Loader2 className="w-10 h-10 text-orange-400 animate-spin" />
|
||||
<p className="text-slate-500">กำลังโหลดคำสั่งซื้อ...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !order) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<ShoppingBag className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ไม่พบคำสั่งซื้อนี้'}</p>
|
||||
<Link to="/shopee" className="btn-primary px-6 py-2 flex items-center gap-2">
|
||||
<ArrowLeft className="w-4 h-4" /> กลับ
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const raw = order.items?.[0]?.rawData ?? {}
|
||||
|
||||
// Financial summary from order meta
|
||||
const totalAmount = order.totalAmount ?? 0
|
||||
const buyerPaidAmount = order.buyerPaidAmount ?? 0
|
||||
const commission = order.commission ?? 0
|
||||
const shippingFee = order.shippingFee ?? 0
|
||||
const shopeeDiscount = parseFloat(raw['ส่วนลดจาก Shopee'] ?? '0') || 0
|
||||
const shippingByShopee = parseFloat(raw['ค่าจัดส่งที่ Shopee ออกให้โดยประมาณ'] ?? '0') || 0
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-8 animate-fade-in pb-16">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
to="/shopee"
|
||||
className="p-2 rounded-xl border border-white/10 hover:bg-white/5 text-slate-400 hover:text-white transition-all"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<Link to="/shopee" className="hover:text-orange-400 transition-colors">Shopee</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white">คำสั่งซื้อ</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold font-mono text-white">{order.orderId}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-4 py-2 rounded-xl text-xs font-bold border ${statusStyle(order.status)}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
{order.trackingNo && (
|
||||
<a
|
||||
href={`https://www.spx.co.th/th/tracking?trackingNumber=${order.trackingNo}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 hover:text-white hover:bg-white/10 text-xs font-medium transition-all"
|
||||
>
|
||||
<Truck className="w-4 h-4" />
|
||||
ติดตามพัสดุ
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Grid */}
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* Left Col */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
|
||||
{/* Order Items */}
|
||||
<section className="admin-card !p-0 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-6 py-4 border-b border-white/5 bg-white/[0.02]">
|
||||
<Package className="w-4 h-4 text-orange-400" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">
|
||||
รายการสินค้า ({order.items?.length ?? 0} รายการ)
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/[0.04]">
|
||||
{(order.items ?? []).map((item: any, idx: number) => (
|
||||
<div key={idx} className="px-6 py-5 flex gap-4 hover:bg-white/[0.01] transition-colors">
|
||||
{/* Index Badge */}
|
||||
<div className="w-8 h-8 rounded-lg bg-orange-500/10 border border-orange-500/20 flex items-center justify-center text-orange-400 font-bold text-xs flex-shrink-0 mt-0.5">
|
||||
{idx + 1}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="font-bold text-white text-sm leading-snug">{item.productName}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.variantName && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 text-[10px] text-slate-400">
|
||||
<Tag className="w-3 h-3" /> {item.variantName}
|
||||
</span>
|
||||
)}
|
||||
{item.parentSku && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 text-[10px] font-mono text-slate-500">
|
||||
<Hash className="w-3 h-3" /> {item.parentSku}
|
||||
</span>
|
||||
)}
|
||||
{item.skuRef && item.skuRef !== item.parentSku && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/5 border border-white/10 text-[10px] font-mono text-slate-500">
|
||||
SKU: {item.skuRef}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right flex-shrink-0 space-y-1">
|
||||
<p className="text-sm font-bold text-emerald-400">฿{fmt(item.totalPayment)}</p>
|
||||
<p className="text-xs text-slate-500">฿{fmt(item.dealPrice)} × {item.quantity}</p>
|
||||
{item.originalPrice !== item.dealPrice && (
|
||||
<p className="text-[10px] text-slate-600 line-through">฿{fmt(item.originalPrice)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Financials Footer */}
|
||||
<div className="px-6 py-5 bg-white/[0.02] border-t border-white/5 space-y-2.5 text-sm">
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>ราคาสินค้า (ชำระโดยผู้ซื้อ)</span>
|
||||
<span className="font-mono">฿{fmt(buyerPaidAmount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>ค่าจัดส่ง (ชำระโดยผู้ซื้อ)</span>
|
||||
<span className="font-mono">฿{fmt(shippingFee)}</span>
|
||||
</div>
|
||||
{shippingByShopee > 0 && (
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>ค่าจัดส่ง (Shopee ออกให้)</span>
|
||||
<span className="font-mono text-blue-400">฿{fmt(shippingByShopee)}</span>
|
||||
</div>
|
||||
)}
|
||||
{shopeeDiscount > 0 && (
|
||||
<div className="flex justify-between text-rose-400">
|
||||
<span>ส่วนลด Shopee</span>
|
||||
<span className="font-mono">-฿{fmt(shopeeDiscount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-rose-400">
|
||||
<span>ค่าคอมมิชชั่น</span>
|
||||
<span className="font-mono">-฿{fmt(commission)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-lg font-bold text-white pt-3 mt-3 border-t border-white/10">
|
||||
<span>ยอดรวมคำสั่งซื้อ</span>
|
||||
<span className="text-emerald-400">฿{fmt(totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Shipping Info */}
|
||||
{raw['ที่อยู่ในการจัดส่ง'] && (
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-cyan-glow" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">ที่อยู่จัดส่ง</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 gap-5">
|
||||
<InfoRow label="ชื่อผู้รับ" value={raw['ชื่อผู้รับ']} />
|
||||
<InfoRow label="เบอร์โทรศัพท์" value={raw['หมายเลขโทรศัพท์']} />
|
||||
<div className="sm:col-span-2">
|
||||
<InfoRow label="ที่อยู่" value={raw['ที่อยู่ในการจัดส่ง']} />
|
||||
</div>
|
||||
<InfoRow label="จังหวัด" value={raw['จังหวัด']} />
|
||||
<InfoRow label="เขต/อำเภอ" value={raw['เขต/อำเภอ']} />
|
||||
<InfoRow label="รหัสไปรษณีย์" value={raw['รหัสไปรษณีย์']} mono />
|
||||
<InfoRow label="ประเทศ" value={raw['ประเทศ']} />
|
||||
</div>
|
||||
{raw['หมายเหตุจากผู้ซื้อ'] && (
|
||||
<div className="p-4 bg-amber-500/5 border border-amber-500/15 rounded-xl">
|
||||
<p className="text-[10px] uppercase font-bold tracking-wider text-amber-500/70 mb-1">หมายเหตุจากผู้ซื้อ</p>
|
||||
<p className="text-sm text-amber-200">{raw['หมายเหตุจากผู้ซื้อ']}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Col */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Order Meta */}
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Receipt className="w-4 h-4 text-orange-400" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">รายละเอียดคำสั่งซื้อ</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<InfoRow label="หมายเลขคำสั่งซื้อ" value={order.orderId} mono />
|
||||
<InfoRow label="วันที่สั่งซื้อ" value={order.orderCreatedAt} />
|
||||
<InfoRow label="เวลาชำระเงิน" value={order.paymentTime} />
|
||||
<InfoRow label="วันที่สั่งซื้อสำเร็จ" value={order.completedAt} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Buyer Info */}
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-cyan-glow" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">ผู้ซื้อ</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<InfoRow label="Username" value={order.buyerUsername} mono />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Payment */}
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4 text-emerald-400" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">การชำระเงิน</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<InfoRow label="ช่องทางชำระเงิน" value={order.paymentMethod} />
|
||||
<InfoRow label="รายละเอียด" value={raw['ช่องทางการชำระเงิน (รายละเอียด)']} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Logistics */}
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Truck className="w-4 h-4 text-blue-400" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">การจัดส่ง</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<InfoRow label="ช่องทางจัดส่ง" value={raw['ตัวเลือกการจัดส่ง']} />
|
||||
<InfoRow label="วิธีจัดส่ง" value={raw['วิธีการจัดส่ง']} />
|
||||
{order.trackingNo && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] uppercase font-bold tracking-widest text-slate-500">หมายเลขติดตาม</p>
|
||||
<a
|
||||
href={`https://www.spx.co.th/th/tracking?trackingNumber=${order.trackingNo}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-mono hover:bg-blue-500/15 transition-colors"
|
||||
>
|
||||
<Truck className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span className="flex-1 break-all">{order.trackingNo}</span>
|
||||
<ExternalLink className="w-3 h-3 flex-shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Fee/Commission Summary */}
|
||||
<section className="admin-card space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<BadgePercent className="w-4 h-4 text-rose-400" />
|
||||
<h2 className="font-bold text-sm uppercase tracking-widest text-slate-300">ค่าธรรมเนียม</h2>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
{[
|
||||
{ label: 'ค่าธรรมเนียม (%)', value: raw['ค่าธรรมเนียม (%)'] },
|
||||
{ label: 'ค่าคอมมิชชั่น', value: `฿${fmt(commission)}` },
|
||||
{ label: 'Transaction Fee', value: raw['Transaction Fee'] ? `฿${fmt(parseFloat(raw['Transaction Fee']))}` : null },
|
||||
{ label: 'ค่าบริการ', value: raw['ค่าบริการ'] ? `฿${fmt(parseFloat(raw['ค่าบริการ']))}` : null },
|
||||
].filter(r => r.value).map(r => (
|
||||
<div key={r.label} className="flex justify-between items-center">
|
||||
<span className="text-slate-400 text-xs">{r.label}</span>
|
||||
<span className="font-mono text-rose-400 text-xs">{r.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, DollarSign, Package, ReceiptText,
|
||||
BadgePercent, ShoppingCart, Loader2, Store, Calendar,
|
||||
} from 'lucide-react';
|
||||
import { SHOPEE_SHOPS } from './ShopeePage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// Color map per shop ID (matches SHOPEE_SHOPS palette)
|
||||
const SHOP_HEX: Record<string, string> = {
|
||||
'hom-aroma': '#f97316', // orange
|
||||
'shop-01': '#a78bfa', // violet
|
||||
'shop-02': '#38bdf8', // sky
|
||||
};
|
||||
const PIE_FALLBACK = ['#22d3ee', '#4ade80', '#f97316', '#818cf8'];
|
||||
|
||||
function shopHex(id: string, idx: number) {
|
||||
return SHOP_HEX[id] ?? PIE_FALLBACK[idx % PIE_FALLBACK.length];
|
||||
}
|
||||
function shopLabel(id: string) {
|
||||
return SHOPEE_SHOPS.find(s => s.id === id)?.label ?? id;
|
||||
}
|
||||
|
||||
// ── Date range helpers ────────────────────────────────────────────────────────
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
function toISO(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISO(now);
|
||||
if (preset === '7d') { const f = new Date(now); f.setDate(f.getDate() - 6); return { from: toISO(f), to: today }; }
|
||||
if (preset === '30d') { const f = new Date(now); f.setDate(f.getDate() - 29); return { from: toISO(f), to: today }; }
|
||||
if (preset === '90d') { const f = new Date(now); f.setDate(f.getDate() - 89); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'thisMonth') { const f = new Date(now.getFullYear(), now.getMonth(), 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastMonth') { const f = new Date(now.getFullYear(), now.getMonth() - 1, 1); const t = new Date(now.getFullYear(), now.getMonth(), 0); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'year') { const f = new Date(now.getFullYear(), 0, 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastYear') { const f = new Date(now.getFullYear() - 1, 0, 1); const t = new Date(now.getFullYear() - 1, 11, 31); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'last12m') { const f = new Date(now); f.setFullYear(f.getFullYear() - 1); return { from: toISO(f), to: today }; }
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
function fmt(val: number, dec = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||
}
|
||||
function fmtB(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function KPICard({ label, value, sub, color = 'text-white', icon: Icon }: { label: string; value: string; sub?: string; color?: string; icon: any }) {
|
||||
return (
|
||||
<div className="relative bg-white/5 border border-white/10 rounded-2xl p-5 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]"><Icon className="w-14 h-14" /></div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-[#0f172a] border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1.5 min-w-[160px]">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white ml-auto">
|
||||
{typeof p.value === 'number' ? (p.value > 1000 ? fmtB(p.value) : fmt(p.value)) : p.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ShopeeOverviewPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const loadData = (from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
fetch(`${API_URL}/api/shopee/analytics-overview${qs}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
};
|
||||
|
||||
useEffect(() => { loadData('', ''); }, []);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(dateFrom, dateTo);
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
<span className="text-slate-500">กำลังรวมข้อมูลทุกร้าน...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<Store className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน'}</p>
|
||||
<Link to="/shopee" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 text-sm">
|
||||
← กลับ
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { byShop, monthly, topProducts, shops, totals } = data;
|
||||
|
||||
// Pie data for revenue share
|
||||
const pieData = byShop.map((s: any) => ({ name: shopLabel(s.shop), value: s.revenue, id: s.shop }));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-orange-500/20 via-violet-500/10 to-sky-500/20 border border-white/10 flex items-center justify-center">
|
||||
<Store className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">Shopee</p>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">ภาพรวมทุกร้าน</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{byShop.map((s: any, i: number) => (
|
||||
<Link
|
||||
key={s.shop}
|
||||
to="/shopee-analytics"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl border text-xs font-bold transition-all hover:opacity-80"
|
||||
style={{ borderColor: `${shopHex(s.shop, i)}40`, backgroundColor: `${shopHex(s.shop, i)}15`, color: shopHex(s.shop, i) }}
|
||||
>
|
||||
{shopLabel(s.shop)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
|
||||
preset === p.key
|
||||
? 'bg-cyan-glow/15 text-cyan-glow border-cyan-glow/30'
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-cyan-glow/40 focus:outline-none"
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-cyan-glow/40 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className="px-4 py-1.5 rounded-lg bg-cyan-glow/10 border border-cyan-glow/20 text-cyan-glow text-xs font-medium hover:bg-cyan-glow/20 transition-all"
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active badge + clear */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData('', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Grand KPI Row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<KPICard label="ยอดขายรวมทุกร้าน" value={fmtB(totals.totalRevenue)} sub={`${fmt(totals.totalOrders)} คำสั่งซื้อ`} color="text-emerald-400" icon={DollarSign} />
|
||||
<KPICard label="รายได้สุทธิรวม" value={fmtB(totals.netRevenue)} sub={`Margin ${fmt(totals.totalRevenue > 0 ? (totals.netRevenue / totals.totalRevenue) * 100 : 0, 1)}%`} color="text-cyan-glow" icon={TrendingUp} />
|
||||
<KPICard label="ค่าคอมมิชชั่นรวม" value={fmtB(totals.totalCommission)} sub={`${fmt(totals.commissionRate, 1)}% ของยอดขาย`} color="text-rose-400" icon={ReceiptText} />
|
||||
<KPICard label="AOV รวม" value={fmtB(totals.aov)} sub={`ยกเลิก ${fmt(totals.cancelRate, 1)}%`} color="text-amber-400" icon={ShoppingCart} />
|
||||
</div>
|
||||
|
||||
{/* Per-Shop Cards */}
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{byShop.map((s: any, i: number) => {
|
||||
const hex = shopHex(s.shop, i);
|
||||
const revShare = totals.totalRevenue > 0 ? (s.revenue / totals.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<div
|
||||
key={s.shop}
|
||||
className="bg-white/5 border border-white/10 rounded-2xl p-5 space-y-4"
|
||||
style={{ borderTopColor: hex, borderTopWidth: '2px' }}
|
||||
>
|
||||
{/* Shop header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-bold text-white text-base">{shopLabel(s.shop)}</p>
|
||||
<p className="text-[10px] font-mono text-slate-500">{s.shop}</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold px-2 py-1 rounded-lg" style={{ backgroundColor: `${hex}20`, color: hex }}>
|
||||
{fmt(revShare, 1)}% of total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
{[
|
||||
{ label: 'ยอดขาย', value: fmtB(s.revenue), color: 'text-emerald-400' },
|
||||
{ label: 'สุทธิ', value: fmtB(s.net), color: 'text-cyan-glow' },
|
||||
{ label: 'คำสั่งซื้อ', value: fmt(s.orders), color: 'text-white' },
|
||||
{ label: 'ค่าคอม', value: fmtB(s.commission), color: 'text-rose-400' },
|
||||
{ label: 'ยกเลิก', value: `${fmt(s.cancelRate, 1)}%`, color: 'text-amber-400' },
|
||||
{ label: 'AOV', value: fmtB(s.aov), color: 'text-purple-400' },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
<p className="text-slate-500 text-[10px] uppercase tracking-wider">{item.label}</p>
|
||||
<p className={`font-bold font-mono ${item.color}`}>{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Revenue bar */}
|
||||
<div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all" style={{ width: `${Math.min(revShare, 100)}%`, backgroundColor: hex }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/shopee-analytics"
|
||||
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-xl text-xs font-medium border transition-all hover:opacity-90"
|
||||
style={{ borderColor: `${hex}30`, backgroundColor: `${hex}10`, color: hex }}
|
||||
>
|
||||
<TrendingUp className="w-3.5 h-3.5" /> ดู Analytics
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Monthly Stacked Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-5 h-5 text-cyan-glow" />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน (แยกตามร้าน)</h2>
|
||||
</div>
|
||||
{monthly.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthly} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barCategoryGap="30%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="month" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => v >= 1000 ? `฿${(v / 1000).toFixed(0)}k` : `฿${v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} formatter={(value) => shopLabel(value.replace('_revenue', ''))} />
|
||||
{shops.map((shop: string, i: number) => (
|
||||
<Bar key={shop} dataKey={`${shop}_revenue`} name={`${shop}_revenue`} stackId="rev" fill={shopHex(shop, i)} radius={i === shops.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]} />
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Revenue Share Pie + Per-shop comparison table */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<BadgePercent className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนยอดขายตามร้าน</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-[220px] flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={pieData} cx="50%" cy="50%" innerRadius={55} outerRadius={90} dataKey="value" nameKey="name" stroke="none" paddingAngle={3}>
|
||||
{pieData.map((entry: any, i: number) => (
|
||||
<Cell key={i} fill={shopHex(entry.id, i)} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }}
|
||||
formatter={(v: any) => fmtB(v)} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{byShop.map((s: any, i: number) => (
|
||||
<div key={s.shop} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="text-slate-300 font-medium w-24">{shopLabel(s.shop)}</span>
|
||||
<span className="font-mono text-white">{fmtB(s.revenue)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Store className="w-5 h-5 text-slate-400" />
|
||||
<h2 className="text-lg font-bold text-white">เปรียบเทียบร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left">ร้าน</th>
|
||||
<th className="pb-3 text-right">ยอดขาย</th>
|
||||
<th className="pb-3 text-right">สุทธิ</th>
|
||||
<th className="pb-3 text-right">ออเดอร์</th>
|
||||
<th className="pb-3 text-right">ยกเลิก</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{byShop.map((s: any, i: number) => (
|
||||
<tr key={s.shop} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="font-medium text-white">{shopLabel(s.shop)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-right font-mono text-emerald-400 font-bold">{fmtB(s.revenue)}</td>
|
||||
<td className="py-3 text-right font-mono text-cyan-glow">{fmtB(s.net)}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300">{fmt(s.orders)}</td>
|
||||
<td className="py-3 text-right font-mono text-rose-400">{fmt(s.cancelRate, 1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Total row */}
|
||||
<tr className="bg-white/[0.03]">
|
||||
<td className="py-3 font-bold text-white text-xs">รวมทั้งหมด</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400">{fmtB(totals.totalRevenue)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-cyan-glow">{fmtB(totals.netRevenue)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-slate-300">{fmt(totals.totalOrders)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-rose-400">{fmt(totals.cancelRate, 1)}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products across all shops */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี — รวมทุกร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left w-8">#</th>
|
||||
<th className="pb-3 text-left">สินค้า</th>
|
||||
<th className="pb-3 text-left">ตัวเลือก</th>
|
||||
<th className="pb-3 text-right">ชิ้น</th>
|
||||
<th className="pb-3 text-right">ยอดรายได้</th>
|
||||
<th className="pb-3 text-right">% รวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = totals.totalRevenue > 0 ? (p.revenue / totals.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3 text-slate-500 font-bold text-xs">{i + 1}</td>
|
||||
<td className="py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="py-3 text-slate-400 text-xs">{p.variant || '—'}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300 text-xs">{fmt(p.qty)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400 text-xs">{fmtB(p.revenue)}</td>
|
||||
<td className="py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-14 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-purple-400/60" style={{ width: `${Math.min(pct * 5, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-400 w-9 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { ShoppingBag, Upload, Table2, TrendingUp, Package, DollarSign, ReceiptText, ChevronLeft, ChevronRight, ExternalLink, Store } from 'lucide-react';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// ── Shop Config ───────────────────────────────────────────────────────────────
|
||||
export const SHOPEE_SHOPS = [
|
||||
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-orange-400', bg: 'bg-orange-500/15', border: 'border-orange-500/20', activeBg: 'bg-orange-500/20', activeBorder: 'border-orange-500/30' },
|
||||
{ id: 'shop-01', label: 'iRelax', color: 'text-violet-400', bg: 'bg-violet-500/15', border: 'border-violet-500/20', activeBg: 'bg-violet-500/20', activeBorder: 'border-violet-500/30' },
|
||||
{ id: 'shop-02', label: 'Shop-02', color: 'text-sky-400', bg: 'bg-sky-500/15', border: 'border-sky-500/20', activeBg: 'bg-sky-500/20', activeBorder: 'border-sky-500/30' },
|
||||
];
|
||||
|
||||
function getShop(id: string) { return SHOPEE_SHOPS.find(s => s.id === id) ?? SHOPEE_SHOPS[0]; }
|
||||
|
||||
function authHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}`,
|
||||
};
|
||||
}
|
||||
|
||||
function fmt(val: number) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
|
||||
}
|
||||
|
||||
// ── Shop Switcher ─────────────────────────────────────────────────────────────
|
||||
function ShopSwitcher({ shop, onChange }: { shop: string; onChange: (s: string) => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Store className="w-4 h-4 text-slate-500" />
|
||||
{SHOPEE_SHOPS.map(s => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => onChange(s.id)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold border transition-all ${shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Summary Cards ─────────────────────────────────────────────────────────────
|
||||
function SummaryCards({ summary, shopColor }: { summary: any | null; shopColor: string }) {
|
||||
if (!summary) return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-5 animate-pulse h-24" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const cards = [
|
||||
{ label: 'คำสั่งซื้อทั้งหมด', value: summary.orderCount.toLocaleString(), sub: `${summary.totalItems?.toLocaleString()} รายการสินค้า`, icon: ShoppingBag, color: shopColor },
|
||||
{ label: 'ยอดชำระจากผู้ซื้อ', value: `฿${fmt(summary.buyerPaidAmount)}`, sub: `ยอดรวม ฿${fmt(summary.totalAmount)}`, icon: DollarSign, color: 'text-emerald-400' },
|
||||
{ label: 'ค่าคอมมิชชั่น', value: `฿${fmt(summary.totalCommission)}`, sub: `ค่าจัดส่ง ฿${fmt(summary.totalShipping)}`, icon: ReceiptText, color: 'text-rose-400' },
|
||||
{ label: 'รายได้รายการสินค้า', value: `฿${fmt(summary.totalItemRevenue)}`, sub: 'ยอดชำระระดับ SKU', icon: Package, color: 'text-amber-400' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{cards.map((c) => (
|
||||
<div key={c.label} className="bg-white/5 border border-white/10 rounded-2xl p-5 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">{c.label}</p>
|
||||
<c.icon className={`w-4 h-4 ${c.color}`} />
|
||||
</div>
|
||||
<p className={`text-xl font-bold ${c.color}`}>{c.value}</p>
|
||||
{c.sub && <p className="text-xs text-slate-500">{c.sub}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Orders Table ──────────────────────────────────────────────────────────────
|
||||
function OrdersTable({ shop, refreshKey }: { shop: string; refreshKey: number }) {
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Reset to page 1 whenever shop or refreshKey changes
|
||||
useEffect(() => { setPage(1); }, [shop, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${API_URL}/api/shopee/orders?page=${page}&limit=20&shop=${shop}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setItems(d.items || []);
|
||||
setTotal(d.total || 0);
|
||||
setTotalPages(d.totalPages || 1);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [shop, page, refreshKey]);
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
if (s.includes('สำเร็จ')) return 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/20';
|
||||
if (s.includes('ได้รับสินค้า')) return 'bg-blue-500/15 text-blue-400 border border-blue-500/20';
|
||||
if (s.includes('ยกเลิก')) return 'bg-rose-500/15 text-rose-400 border border-rose-500/20';
|
||||
return 'bg-amber-500/15 text-amber-400 border border-amber-500/20';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-slate-400">
|
||||
ทั้งหมด <span className="text-white font-bold">{total.toLocaleString()}</span> คำสั่งซื้อ
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)}
|
||||
className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-slate-400 text-xs">{page} / {totalPages}</span>
|
||||
<button disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}
|
||||
className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3 whitespace-nowrap">Order ID</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">สถานะ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">ผู้ซื้อ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">วันที่สั่ง</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">SKUs</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ยอดรวม (฿)</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ค่าคอมฯ (฿)</th>
|
||||
<th className="px-4 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={8} className="px-4 py-10 text-center text-slate-500">กำลังโหลด...</td></tr>
|
||||
) : items.length === 0 ? (
|
||||
<tr><td colSpan={8} className="px-4 py-16 text-center text-slate-500">ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน</td></tr>
|
||||
) : items.map((order) => (
|
||||
<tr key={order.orderId} className="border-b border-white/5 hover:bg-white/[0.04] transition-colors group">
|
||||
<td className="px-4 py-3 font-mono text-xs text-orange-400 font-bold">{order.orderId}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium whitespace-nowrap border ${statusColor(order.status)}`}>
|
||||
{order.status.length > 20 ? order.status.slice(0, 20) + '…' : order.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{order.buyerUsername}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">{order.orderCreatedAt}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-slate-400 text-xs border border-white/10">
|
||||
<Package className="w-3 h-3" />{order.items?.length ?? 0} SKU
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">{fmt(order.totalAmount)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-rose-400 text-xs">{fmt(order.commission)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<Link
|
||||
to={`/shopee/${encodeURIComponent(order.orderId)}`}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-slate-400 text-xs hover:bg-orange-500/10 hover:border-orange-500/20 hover:text-orange-400 transition-all opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
รายละเอียด <ExternalLink className="w-3 h-3" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Import Panel ──────────────────────────────────────────────────────────────
|
||||
function ImportPanel({ shop, onSuccess }: { shop: string; onSuccess: () => void }) {
|
||||
const [previewRows, setPreviewRows] = useState<any[]>([]);
|
||||
const [allRows, setAllRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||
const [sheetInfo, setSheetInfo] = useState('');
|
||||
const shopCfg = getShop(shop);
|
||||
|
||||
const readFile = (f: File) => new Promise<XLSX.WorkBook>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = evt => {
|
||||
try { resolve(XLSX.read(evt.target?.result, { type: 'array', cellDates: false })); }
|
||||
catch (e) { reject(e); }
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(f);
|
||||
});
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
setMsg(null); setAllRows([]); setPreviewRows([]); setSheetInfo('');
|
||||
try {
|
||||
const wb = await readFile(f);
|
||||
const sheetName = wb.SheetNames.find(n => n.toLowerCase().includes('order')) ?? wb.SheetNames[0];
|
||||
const ws = wb.Sheets[sheetName];
|
||||
setSheetInfo(`Sheet: "${sheetName}" | ${f.name}`);
|
||||
const rawRows = XLSX.utils.sheet_to_json<any[]>(ws, { header: 1, defval: null, blankrows: false, raw: false }) as any[][];
|
||||
if (rawRows.length < 2) { setMsg({ text: 'ไม่พบข้อมูลในไฟล์', type: 'error' }); return; }
|
||||
const headers = (rawRows[0] as any[]).map(h => h != null ? String(h).trim() : '');
|
||||
const rows: any[] = [];
|
||||
for (let i = 1; i < rawRows.length; i++) {
|
||||
const cells = rawRows[i] as any[];
|
||||
if (cells.every(c => c === null || c === undefined || c === '')) continue;
|
||||
const row: any = {};
|
||||
headers.forEach((h, idx) => { row[h] = cells[idx] ?? null; });
|
||||
if (row['หมายเลขคำสั่งซื้อ']) rows.push(row);
|
||||
}
|
||||
setAllRows(rows);
|
||||
setPreviewRows(rows.slice(0, 5));
|
||||
} catch (err: any) {
|
||||
setMsg({ text: `อ่านไฟล์ไม่ได้: ${err.message}`, type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (allRows.length === 0) return;
|
||||
setLoading(true); setMsg(null);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/shopee/import/orders`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ rows: allRows, shop }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setMsg({ text: result.message, type: 'success' });
|
||||
setAllRows([]); setPreviewRows([]); setSheetInfo('');
|
||||
const inp = document.getElementById('shopee-file-input') as HTMLInputElement;
|
||||
if (inp) inp.value = '';
|
||||
onSuccess();
|
||||
} else throw new Error(result.message);
|
||||
} catch (e: any) {
|
||||
setMsg({ text: e.message, type: 'error' });
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const uniqueOrdersCount = new Set(allRows.map(r => r['หมายเลขคำสั่งซื้อ'])).size;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Target shop indicator */}
|
||||
<div className={`flex items-center gap-3 px-4 py-3 rounded-xl ${shopCfg.bg} border ${shopCfg.border}`}>
|
||||
<Store className={`w-4 h-4 ${shopCfg.color}`} />
|
||||
<p className="text-sm font-medium text-white">
|
||||
กำลัง Import เข้าร้าน: <span className={`font-bold ${shopCfg.color}`}>{shopCfg.label}</span>
|
||||
</p>
|
||||
<span className={`ml-auto text-xs font-mono ${shopCfg.color} opacity-60`}>{shopCfg.id}</span>
|
||||
</div>
|
||||
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm ${msg.type === 'success' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label
|
||||
htmlFor="shopee-file-input"
|
||||
className="flex flex-col items-center justify-center border-2 border-dashed border-white/10 hover:border-orange-500/30 rounded-2xl p-12 bg-black/20 cursor-pointer transition-all group gap-4"
|
||||
>
|
||||
<div className={`w-16 h-16 rounded-2xl ${shopCfg.bg} border ${shopCfg.border} flex items-center justify-center group-hover:opacity-80 transition-opacity`}>
|
||||
<Upload className={`w-8 h-8 ${shopCfg.color}`} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white font-bold">คลิกเพื่อเลือกไฟล์ Excel</p>
|
||||
<p className="text-slate-500 text-sm mt-1">รองรับ .xlsx จาก Shopee Seller Center (Orders Report)</p>
|
||||
</div>
|
||||
<input id="shopee-file-input" type="file" accept=".xlsx,.xls" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
|
||||
{sheetInfo && (
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/10 text-xs font-mono text-slate-400">
|
||||
<span>📋 {sheetInfo}</span>
|
||||
<span className={`font-bold ${allRows.length > 0 ? 'text-emerald-400' : 'text-amber-400'}`}>
|
||||
{allRows.length} แถว → {uniqueOrdersCount} คำสั่งซื้อ
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewRows.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-400 font-medium">Preview (5 แถวแรก):</p>
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-xs text-left">
|
||||
<thead className="bg-white/5 text-slate-400 border-b border-white/10">
|
||||
<tr>
|
||||
{['หมายเลขคำสั่งซื้อ', 'สถานะการสั่งซื้อ', 'ชื่อสินค้า', 'ชื่อตัวเลือก', 'ราคาขาย', 'จำนวน', 'ยอดชำระเงิน'].map(k => (
|
||||
<th key={k} className="px-3 py-2 whitespace-nowrap">{k}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{previewRows.map((row, i) => (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/5">
|
||||
{['หมายเลขคำสั่งซื้อ', 'สถานะการสั่งซื้อ', 'ชื่อสินค้า', 'ชื่อตัวเลือก', 'ราคาขาย', 'จำนวน', 'ยอดชำระเงิน'].map(k => (
|
||||
<td key={k} className="px-3 py-2 text-slate-300 max-w-[200px] truncate" title={String(row[k] ?? '')}>
|
||||
{String(row[k] ?? '-')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={loading}
|
||||
className={`px-8 py-2.5 font-bold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 ${shopCfg.activeBg} ${shopCfg.color} border ${shopCfg.activeBorder} hover:opacity-90`}
|
||||
>
|
||||
{loading ? <><span className="animate-spin">⏳</span> กำลัง Import...</> : <><Upload className="w-4 h-4" /> Import {uniqueOrdersCount} คำสั่งซื้อ ({allRows.length} แถว) → {shopCfg.label}</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
type ViewMode = 'list' | 'import';
|
||||
|
||||
export default function ShopeePage() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [summary, setSummary] = useState<any>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [shop, setShop] = useState('hom-aroma');
|
||||
const shopCfg = getShop(shop);
|
||||
|
||||
const loadSummary = (s: string) => {
|
||||
setSummary(null);
|
||||
fetch(`${API_URL}/api/shopee/summary?shop=${s}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(setSummary)
|
||||
.catch(() => { });
|
||||
};
|
||||
|
||||
useEffect(() => { loadSummary(shop); }, [shop, refreshKey]);
|
||||
|
||||
const handleShopChange = (s: string) => {
|
||||
setShop(s);
|
||||
setViewMode('list');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl ${shopCfg.bg} border ${shopCfg.border} flex items-center justify-center`}>
|
||||
<ShoppingBag className={`w-5 h-5 ${shopCfg.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">Shopee</h1>
|
||||
<p className={`text-xs uppercase tracking-widest font-bold ${shopCfg.color}`}>{shopCfg.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* Shop Switcher */}
|
||||
<ShopSwitcher shop={shop} onChange={handleShopChange} />
|
||||
|
||||
<div className="h-5 w-px bg-white/10 hidden sm:block" />
|
||||
|
||||
{/* View Switcher */}
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'list' ? 'bg-cyan-glow/15 text-cyan-glow border border-cyan-glow/30' : 'text-slate-400 hover:text-white bg-white/5 border border-white/10'}`}
|
||||
>
|
||||
<Table2 className="w-4 h-4" /> ดูข้อมูล
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('import')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'import' ? `${shopCfg.activeBg} ${shopCfg.color} border ${shopCfg.activeBorder}` : 'text-slate-400 hover:text-white bg-white/5 border border-white/10'}`}
|
||||
>
|
||||
<Upload className="w-4 h-4" /> Import ไฟล์
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<SummaryCards summary={summary} shopColor={shopCfg.color} />
|
||||
|
||||
{/* Content Card */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden backdrop-blur-sm">
|
||||
<div className="flex items-center gap-3 px-6 py-4 border-b border-white/10 bg-white/[0.02]">
|
||||
{viewMode === 'list' ? (
|
||||
<>
|
||||
<TrendingUp className={`w-4 h-4 ${shopCfg.color}`} />
|
||||
<h2 className="font-bold text-white">คำสั่งซื้อ — {shopCfg.label}</h2>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Upload className={`w-4 h-4 ${shopCfg.color}`} />
|
||||
<h2 className="font-bold text-white">นำเข้าข้อมูลจาก Shopee</h2>
|
||||
<span className="text-xs text-slate-500">อัพโหลดไฟล์ Order จาก Shopee Seller Center</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{viewMode === 'list' ? (
|
||||
<OrdersTable key={`${shop}-${refreshKey}`} shop={shop} refreshKey={refreshKey} />
|
||||
) : (
|
||||
<ImportPanel shop={shop} onSuccess={() => { setRefreshKey(k => k + 1); setViewMode('list'); }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { API_BASE, type StoreSettings } from '@irelax/types'
|
||||
import { Save, Loader2, Landmark, Truck } from 'lucide-react'
|
||||
|
||||
export default function SystemSettingsPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Form State
|
||||
const [shippingFee, setShippingFee] = useState<string>('')
|
||||
const [bankName, setBankName] = useState('')
|
||||
const [bankAccountName, setBankAccountName] = useState('')
|
||||
const [bankAccountNumber, setBankAccountNumber] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/settings`)
|
||||
.then(r => r.json())
|
||||
.then((data: StoreSettings) => {
|
||||
setShippingFee(data.shippingFee.toString())
|
||||
setBankName(data.bankName)
|
||||
setBankAccountName(data.bankAccountName)
|
||||
setBankAccountNumber(data.bankAccountNumber)
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
shippingFee: parseFloat(shippingFee) || 0,
|
||||
bankName,
|
||||
bankAccountName,
|
||||
bankAccountNumber
|
||||
})
|
||||
})
|
||||
if (res.ok) alert('Settings saved successfully!')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to save settings')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-20">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-8 animate-fade-in">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">System Settings</h1>
|
||||
<p className="text-slate-500 text-sm">Configure shipping rules and payment banking details.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-8">
|
||||
|
||||
{/* Shipping Settings */}
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
<div className="flex items-center gap-3 border-b border-white/5 pb-4">
|
||||
<div className="p-2 bg-cyan-glow/10 rounded-lg">
|
||||
<Truck className="w-5 h-5 text-cyan-glow" />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Shipping Configuration</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Flat Rate Shipping Fee (THB)</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
min="0"
|
||||
value={shippingFee}
|
||||
onChange={e => setShippingFee(e.target.value)}
|
||||
className="admin-input w-full max-w-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500">This fee will be applied uniformly to all orders during checkout.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bank Settings */}
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
<div className="flex items-center gap-3 border-b border-white/5 pb-4">
|
||||
<div className="p-2 bg-cyan-glow/10 rounded-lg">
|
||||
<Landmark className="w-5 h-5 text-cyan-glow" />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Banking Details</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Bank Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={bankName}
|
||||
onChange={e => setBankName(e.target.value)}
|
||||
className="admin-input w-full"
|
||||
placeholder="e.g. KASIKORNBANK (KBANK)"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Account Number</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={bankAccountNumber}
|
||||
onChange={e => setBankAccountNumber(e.target.value)}
|
||||
className="admin-input w-full font-mono font-bold"
|
||||
placeholder="012-3-45678-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Account Holder Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={bankAccountName}
|
||||
onChange={e => setBankAccountName(e.target.value)}
|
||||
className="admin-input w-full max-w-sm"
|
||||
placeholder="Company Co., Ltd."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="btn-primary py-4 px-12 flex items-center gap-2 ml-auto text-sm"
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Save Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell
|
||||
} from 'recharts';
|
||||
import { TrendingUp, PackageOpen, Tag, PieChart as PieChartIcon } from 'lucide-react';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
function formatCurrency(val: number) {
|
||||
return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB', maximumFractionDigits: 0 }).format(val);
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
const COLORS = ['#22d3ee', '#818cf8', '#c084fc', '#f472b6', '#fb923c', '#facc15', '#4ade80'];
|
||||
|
||||
export default function TiktokAnalysisPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/api/tiktok/analysis/sku`, { headers: authHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(d => {
|
||||
setData(d);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center text-slate-400 py-20 animate-pulse">กำลังโหลดข้อมูลวิเคราะห์...</div>;
|
||||
}
|
||||
|
||||
if (!data || !data.summary) {
|
||||
return <div className="text-center text-slate-400 py-20">ไม่มีข้อมูลสำหรับการวิเคราะห์ กรุณา Import ข้อมูล SKU ก่อน</div>;
|
||||
}
|
||||
|
||||
const { summary, topGmv, topVolume, scentAnalysis } = data;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight flex items-center gap-3">
|
||||
<TrendingUp className="text-cyan-glow w-8 h-8" />
|
||||
Product SKU Analysis
|
||||
</h1>
|
||||
<p className="text-slate-400 mt-2">วิเคราะห์ข้อมูลเชิงลึกของสินค้าย่อย (SKU) เพื่อหาโอกาสในการเพิ่มยอดขาย</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10"><TrendingUp className="w-16 h-16" /></div>
|
||||
<p className="text-sm text-slate-400 font-medium mb-1">Total GMV (SKUs)</p>
|
||||
<p className="text-2xl font-bold text-white">{formatCurrency(summary.totalGmv || 0)}</p>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10"><PackageOpen className="w-16 h-16" /></div>
|
||||
<p className="text-sm text-slate-400 font-medium mb-1">Items Sold</p>
|
||||
<p className="text-2xl font-bold text-white">{(summary.totalItems || 0).toLocaleString()} ชิ้น</p>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10"><Tag className="w-16 h-16" /></div>
|
||||
<p className="text-sm text-slate-400 font-medium mb-1">Avg. Price / Item</p>
|
||||
<p className="text-2xl font-bold text-emerald-400">{formatCurrency(summary.avgPrice || 0)}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">ราคาขายเฉลี่ยต่อชิ้น</p>
|
||||
</div>
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10"><PieChartIcon className="w-16 h-16" /></div>
|
||||
<p className="text-sm text-slate-400 font-medium mb-1">Average Order Value (AOV)</p>
|
||||
<p className="text-2xl font-bold text-amber-400">{formatCurrency(summary.avgAov || 0)}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">ยอดบิลเฉลี่ยต่อออเดอร์</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Top 10 by GMV Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-6">Top 10 SKUs by GMV (สร้างยอดขายสูงสุด)</h2>
|
||||
<div className="h-[350px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={topGmv} layout="vertical" margin={{ top: 0, right: 30, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff10" horizontal={false} />
|
||||
<XAxis type="number" stroke="#94a3b8" tickFormatter={(val) => `฿${val/1000}k`} />
|
||||
<YAxis dataKey="skuName" type="category" stroke="#94a3b8" width={200} tick={{ fontSize: 11 }} tickFormatter={(val) => val.length > 25 ? val.substring(0, 25) + '...' : val} />
|
||||
<Tooltip
|
||||
cursor={{ fill: '#ffffff10' }}
|
||||
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }}
|
||||
formatter={(value: any) => [formatCurrency(value), 'GMV']}
|
||||
/>
|
||||
<Bar dataKey="gmv" fill="#22d3ee" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 10 by Volume Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-6">Top 10 SKUs by Volume (ขายออกไวที่สุด)</h2>
|
||||
<div className="h-[350px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={topVolume} layout="vertical" margin={{ top: 0, right: 30, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff10" horizontal={false} />
|
||||
<XAxis type="number" stroke="#94a3b8" />
|
||||
<YAxis dataKey="skuName" type="category" stroke="#94a3b8" width={200} tick={{ fontSize: 11 }} tickFormatter={(val) => val.length > 25 ? val.substring(0, 25) + '...' : val} />
|
||||
<Tooltip
|
||||
cursor={{ fill: '#ffffff10' }}
|
||||
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }}
|
||||
formatter={(value: any) => [`${value} ชิ้น`, 'Items Sold']}
|
||||
/>
|
||||
<Bar dataKey="itemsSold" fill="#818cf8" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scent / Variant Analysis */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-6">Scent / Variant Analysis (กลิ่นยอดนิยม)</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-1 h-[300px] flex items-center justify-center">
|
||||
{scentAnalysis?.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={scentAnalysis.slice(0, 7)}
|
||||
cx="50%" cy="50%"
|
||||
innerRadius={60} outerRadius={90}
|
||||
paddingAngle={2}
|
||||
dataKey="totalGmv"
|
||||
nameKey="scentName"
|
||||
stroke="none"
|
||||
>
|
||||
{scentAnalysis.map((_: any, index: number) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px' }}
|
||||
formatter={(value: any) => formatCurrency(value)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<p className="text-slate-500">ไม่สามารถแยกกลิ่นจากชื่อ SKU ได้ (ชื่ออาจไม่มี ': ')</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="lg:col-span-2 overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-black/20">
|
||||
<tr>
|
||||
<th className="px-4 py-3 rounded-tl-lg">กลิ่น (Variant)</th>
|
||||
<th className="px-4 py-3">Total GMV</th>
|
||||
<th className="px-4 py-3">Items Sold</th>
|
||||
<th className="px-4 py-3">Avg Price</th>
|
||||
<th className="px-4 py-3 rounded-tr-lg">SKU Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{scentAnalysis?.map((scent: any, i: number) => (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/5">
|
||||
<td className="px-4 py-3 text-white font-medium flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: COLORS[i % COLORS.length] }}></div>
|
||||
{scent.scentName || 'Uncategorized'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-cyan-400 font-mono">{formatCurrency(scent.totalGmv)}</td>
|
||||
<td className="px-4 py-3 text-slate-300 font-mono">{scent.totalItems}</td>
|
||||
<td className="px-4 py-3 text-slate-300 font-mono">
|
||||
{formatCurrency(scent.totalItems > 0 ? scent.totalGmv / scent.totalItems : 0)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-400">{scent.skuCount} รายการ</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!scentAnalysis || scentAnalysis.length === 0) && (
|
||||
<tr><td colSpan={5} className="px-4 py-8 text-center text-slate-500">No data available</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, LineChart, Line, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, ShoppingBag, DollarSign, ReceiptText, Package,
|
||||
ArrowLeft, BadgePercent, ShoppingCart, XCircle, CheckCircle2, Loader2,
|
||||
Calendar, Store,
|
||||
} from 'lucide-react';
|
||||
import { TIKTOK_SHOPS } from './TiktokOrderPage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
const COLORS = ['#22d3ee', '#4ade80', '#f97316', '#818cf8', '#f472b6', '#facc15', '#fb923c'];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
function fmt(val: number, decimals = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(val);
|
||||
}
|
||||
function fmtCurrency(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function toISODate(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISODate(now);
|
||||
if (preset === '7d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 6);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '30d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 29);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === '90d') {
|
||||
const from = new Date(now); from.setDate(from.getDate() - 89);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'thisMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastMonth') {
|
||||
const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const to = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'year') {
|
||||
const from = new Date(now.getFullYear(), 0, 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
if (preset === 'lastYear') {
|
||||
const from = new Date(now.getFullYear() - 1, 0, 1);
|
||||
const to = new Date(now.getFullYear() - 1, 11, 31);
|
||||
return { from: toISODate(from), to: toISODate(to) };
|
||||
}
|
||||
if (preset === 'last12m') {
|
||||
const from = new Date(now); from.setFullYear(from.getFullYear() - 1);
|
||||
return { from: toISODate(from), to: today };
|
||||
}
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function StatCard({
|
||||
label, value, sub, icon: Icon, color = 'text-white', bgColor = 'from-white/5',
|
||||
}: { label: string; value: string | number; sub?: string; icon: any; color?: string; bgColor?: string }) {
|
||||
return (
|
||||
<div className={`relative bg-gradient-to-br ${bgColor} to-transparent border border-white/10 rounded-2xl p-5 overflow-hidden`}>
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]">
|
||||
<Icon className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-night-900 border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white">{typeof p.value === 'number' && p.name?.includes('฿') ? fmtCurrency(p.value) : fmt(p.value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function TiktokAnalyticsPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Shop selector
|
||||
const [shop, setShop] = useState('hom-aroma');
|
||||
const shopCfg = TIKTOK_SHOPS.find(s => s.id === shop) ?? TIKTOK_SHOPS[0];
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const buildUrl = useCallback((s: string, from: string, to: string) => {
|
||||
const params = new URLSearchParams({ shop: s });
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
return `${API_URL}/api/tiktok-order/analytics?${params}`;
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback((s: string, from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
fetch(buildUrl(s, from, to), { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
}, [buildUrl]);
|
||||
|
||||
// Reload on shop or initial mount
|
||||
useEffect(() => { loadData(shop, dateFrom, dateTo); }, [shop]);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(shop, range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(shop, dateFrom, dateTo);
|
||||
};
|
||||
|
||||
const handleShopChange = (s: string) => {
|
||||
setShop(s);
|
||||
// reset dates so we don't carry over filter from previous shop
|
||||
setPreset('all'); setDateFrom(''); setDateTo('');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className={`w-8 h-8 ${shopCfg.color} animate-spin`} />
|
||||
<span className="text-slate-500">กำลังคำนวณข้อมูลวิเคราะห์...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<ShoppingBag className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ไม่มีข้อมูล — กรุณา Import ไฟล์ TikTok ก่อน'}</p>
|
||||
<Link to="/tiktok-order" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 hover:text-white text-sm">
|
||||
<ArrowLeft className="w-4 h-4" /> กลับหน้า TikTok Orders
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, monthly, statusBreakdown, paymentBreakdown, topProducts } = data;
|
||||
|
||||
// Prepare monthly chart (revenue + net bars, orders line)
|
||||
const monthlyChart = monthly.map((m: any) => ({
|
||||
...m,
|
||||
net: m.revenue - m.platformDiscount - m.sellerDiscount,
|
||||
label: m.month,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/tiktok-order" className="p-2 rounded-xl border border-white/10 hover:bg-white/5 text-slate-400 hover:text-white transition-all">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 uppercase tracking-widest mb-1">
|
||||
<Link to="/tiktok-order" className={`hover:${shopCfg.color} transition-colors`}>TikTok</Link>
|
||||
<span>/</span>
|
||||
<span className="text-white">Analytics</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight flex items-center gap-2">
|
||||
<TrendingUp className={`w-6 h-6 ${shopCfg.color}`} />
|
||||
วิเคราะห์ยอดขาย —
|
||||
<span className={shopCfg.color}>{shopCfg.label}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shop Switcher */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Store className="w-4 h-4 text-slate-500" />
|
||||
{TIKTOK_SHOPS.map(s => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => handleShopChange(s.id)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold border transition-all ${
|
||||
shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
preset === p.key
|
||||
? `${shopCfg.activeBg} ${shopCfg.color} ${shopCfg.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={e => setDateFrom(e.target.value)}
|
||||
className={`px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:${shopCfg.activeBorder} focus:outline-none`}
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={e => setDateTo(e.target.value)}
|
||||
className={`px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:${shopCfg.activeBorder} focus:outline-none`}
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className={`px-4 py-1.5 rounded-lg ${shopCfg.activeBg} ${shopCfg.activeBorder} ${shopCfg.color} text-xs font-medium hover:opacity-80 transition-all`}
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active filter badge */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData(shop, '', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Row 1 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="ยอดขายรวม (Order Amount)" value={fmtCurrency(summary.totalRevenue)} icon={DollarSign} color="text-emerald-400" bgColor="from-emerald-500/10" sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} />
|
||||
<StatCard label="ยอดสุทธิก่อนหักคอมมิชชั่น" value={fmtCurrency(summary.netRevenue)} icon={TrendingUp} color="text-cyan-glow" bgColor="from-cyan-500/10" sub={`รายได้หลังหักค่าส่งจัดส่ง`} />
|
||||
<StatCard label="ส่วนลด Platform (TikTok ออก)" value={fmtCurrency(summary.totalPlatformDisc)} icon={BadgePercent} color="text-purple-400" bgColor="from-purple-500/10" sub="TikTok อุดหนุนส่วนลดให้" />
|
||||
<StatCard label="ส่วนลดร้านค้า (เราออก)" value={fmtCurrency(summary.totalSellerDisc)} icon={ReceiptText} color="text-rose-400" bgColor="from-rose-500/10" sub="ส่วนลดที่เราให้ลูกค้า" />
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Row 2 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard label="คำสั่งซื้อสำเร็จ" value={fmt(summary.completedOrders)} icon={CheckCircle2} color="text-emerald-400" sub={`จากทั้งหมด ${fmt(summary.totalOrders)}`} />
|
||||
<StatCard label="คำสั่งซื้อยกเลิก" value={fmt(summary.cancelledOrders)} icon={XCircle} color="text-rose-400" sub={`${fmt(summary.cancelRate, 1)}% ของทั้งหมด`} />
|
||||
<StatCard label="ค่าจัดส่งรวม" value={fmtCurrency(summary.totalShipping)} icon={Package} color="text-amber-400" sub="ค่าส่งที่เก็บจากผู้ซื้อ" />
|
||||
<StatCard label="AOV (เฉลี่ยต่อออเดอร์)" value={fmtCurrency(summary.aov)} icon={ShoppingCart} color="text-blue-400" sub="Average Order Value" />
|
||||
</div>
|
||||
|
||||
{/* Monthly Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className={`w-5 h-5 ${shopCfg.color}`} />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน</h2>
|
||||
</div>
|
||||
{monthlyChart.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barGap={3}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => `฿${v >= 1000 ? `${(v / 1000).toFixed(0)}k` : v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} />
|
||||
<Bar dataKey="revenue" name="฿ ยอดขายรวม" fill="#4ade80" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="platformDiscount" name="฿ ส่วนลด Platform" fill="#c084fc" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="sellerDiscount" name="฿ ส่วนลดร้านค้า" fill="#fb7185" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Orders per month line chart */}
|
||||
{monthlyChart.length > 1 && (
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ShoppingBag className="w-5 h-5 text-cyan-glow" />
|
||||
<h2 className="text-lg font-bold text-white">จำนวนคำสั่งซื้อรายเดือน</h2>
|
||||
</div>
|
||||
<div className="h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={monthlyChart} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="label" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line type="monotone" dataKey="orders" name="คำสั่งซื้อ" stroke={shop === 'hom-aroma' ? '#fb7185' : '#a78bfa'} strokeWidth={2} dot={{ r: 4 }} />
|
||||
<Line type="monotone" dataKey="items" name="จำนวนสินค้า (ชิ้น)" stroke="#818cf8" strokeWidth={2} dot={{ r: 4, fill: '#818cf8' }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status + Payment row */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Status Breakdown */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนสถานะคำสั่งซื้อ</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 items-center">
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={statusBreakdown} cx="50%" cy="50%" innerRadius={50} outerRadius={80} dataKey="count" nameKey="status" stroke="none" paddingAngle={2}>
|
||||
{statusBreakdown.map((_: any, i: number) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{statusBreakdown.map((s: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: COLORS[i % COLORS.length] }} />
|
||||
<span className="text-slate-300 flex-1 truncate" title={s.status}>{s.status}</span>
|
||||
<span className="font-bold text-white">{s.count}</span>
|
||||
<span className="text-slate-500">({fmt((s.count / summary.totalOrders) * 100, 0)}%)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<DollarSign className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">ช่องทางการชำระเงิน</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{paymentBreakdown.map((p: any, i: number) => {
|
||||
const pct = summary.totalOrders > 0 ? (p.count / summary.totalOrders) * 100 : 0;
|
||||
return (
|
||||
<div key={i} className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-slate-300 truncate max-w-[160px]" title={p.method}>{p.method || 'ไม่ระบุ'}</span>
|
||||
<span className="text-slate-400 ml-2">{p.count} ออเดอร์ · {fmt(pct, 0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{ width: `${pct}%`, backgroundColor: COLORS[i % COLORS.length] }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี (by ยอดเงิน)</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/[0.03] border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3">#</th>
|
||||
<th className="px-4 py-3">สินค้า</th>
|
||||
<th className="px-4 py-3">ตัวเลือก</th>
|
||||
<th className="px-4 py-3 text-right">ชิ้น</th>
|
||||
<th className="px-4 py-3 text-right">ยอดรายได้</th>
|
||||
<th className="px-4 py-3 text-right">% จากยอดรวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<span className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-xs font-bold text-slate-400">
|
||||
{i + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{p.variation || '—'}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-slate-300">{fmt(p.qty)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">{fmtCurrency(p.revenue)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-16 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-emerald-400/60" style={{ width: `${Math.min(pct * 3, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-10 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cost Waterfall Summary */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ReceiptText className="w-5 h-5 text-rose-400" />
|
||||
<h2 className="text-lg font-bold text-white">สรุปค่าใช้จ่ายและส่วนลด</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{[
|
||||
{ label: 'ยอดขาย Gross', value: summary.totalRevenue, color: 'text-emerald-400', bar: 'bg-emerald-400' },
|
||||
{ label: 'หัก: ค่าจัดส่ง (ผู้ซื้อจ่าย)', value: -summary.totalShipping, color: 'text-orange-400', bar: 'bg-orange-400' },
|
||||
{ label: 'หัก: ส่วนลดร้านค้า', value: -summary.totalSellerDisc, color: 'text-rose-400', bar: 'bg-rose-400' },
|
||||
{ label: 'ยอดสุทธิก่อนหักคอมมิชชั่น', value: summary.netRevenue, color: 'text-cyan-glow', bar: 'bg-cyan-glow', highlight: true },
|
||||
].map((item, i) => (
|
||||
<div key={i} className={`p-4 rounded-xl border ${item.highlight ? 'border-cyan-glow/20 bg-cyan-glow/5' : 'border-white/10 bg-white/[0.02]'}`}>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{item.label}</p>
|
||||
<p className={`text-xl font-bold font-mono ${item.color}`}>{fmtCurrency(Math.abs(item.value))}</p>
|
||||
{summary.totalRevenue > 0 && (
|
||||
<p className="text-[10px] text-slate-600 mt-1">{fmt(Math.abs(item.value / summary.totalRevenue) * 100, 1)}% ของยอด</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-6 p-4 rounded-xl border border-purple-500/20 bg-purple-500/5">
|
||||
<BadgePercent className="w-8 h-8 text-purple-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">ส่วนลด Platform สนับสนุน (TikTok ออกให้)</p>
|
||||
<p className="text-xl font-bold text-purple-400">{fmtCurrency(summary.totalPlatformDisc)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Papa from 'papaparse';
|
||||
import {
|
||||
ShoppingBag, Upload, Table2, TrendingUp, Package, DollarSign,
|
||||
ReceiptText, ChevronLeft, ChevronRight, ExternalLink, Store, Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// ── Shop Config ───────────────────────────────────────────────────────────────
|
||||
export const TIKTOK_SHOPS = [
|
||||
{ id: 'hom-aroma', label: 'Hom Aroma', color: 'text-rose-400', bg: 'bg-rose-500/15', border: 'border-rose-500/20', activeBg: 'bg-rose-500/20', activeBorder: 'border-rose-500/30' },
|
||||
{ id: 'shop-01', label: 'Shop-01', color: 'text-violet-400', bg: 'bg-violet-500/15', border: 'border-violet-500/20', activeBg: 'bg-violet-500/20', activeBorder: 'border-violet-500/30' },
|
||||
];
|
||||
|
||||
function getShop(id: string) { return TIKTOK_SHOPS.find(s => s.id === id) ?? TIKTOK_SHOPS[0]; }
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
function fmt(val: number) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val);
|
||||
}
|
||||
|
||||
// ── Shop Switcher ─────────────────────────────────────────────────────────────
|
||||
function ShopSwitcher({ shop, onChange }: { shop: string; onChange: (s: string) => void }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Store className="w-4 h-4 text-slate-500" />
|
||||
{TIKTOK_SHOPS.map(s => (
|
||||
<button key={s.id} onClick={() => onChange(s.id)}
|
||||
className={`px-3 py-1.5 rounded-xl text-xs font-bold border transition-all ${shop === s.id
|
||||
? `${s.activeBg} ${s.color} ${s.activeBorder}`
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'}`}>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Summary Cards ─────────────────────────────────────────────────────────────
|
||||
function SummaryCards({ summary, shopColor }: { summary: any | null; shopColor: string }) {
|
||||
if (!summary) return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => <div key={i} className="bg-white/5 border border-white/10 rounded-2xl p-5 animate-pulse h-24" />)}
|
||||
</div>
|
||||
);
|
||||
const cards = [
|
||||
{ label: 'คำสั่งซื้อทั้งหมด', value: summary.orderCount.toLocaleString(), sub: `${summary.totalItems?.toLocaleString()} รายการ SKU`, icon: ShoppingBag, color: shopColor },
|
||||
{ label: 'ยอดรวม (Order Amount)', value: `฿${fmt(summary.totalAmount)}`, sub: `SKU Revenue ฿${fmt(summary.totalSkuRevenue)}`, icon: DollarSign, color: 'text-emerald-400' },
|
||||
{ label: 'ค่าส่งรวม', value: `฿${fmt(summary.totalShipping)}`, sub: `คืนเงิน ฿${fmt(summary.totalRefund)}`, icon: ReceiptText, color: 'text-amber-400' },
|
||||
{ label: 'ภาษีรวม', value: `฿${fmt(summary.totalTaxes)}`, sub: 'Tax collected', icon: Package, color: 'text-blue-400' },
|
||||
];
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{cards.map((c) => (
|
||||
<div key={c.label} className="bg-white/5 border border-white/10 rounded-2xl p-5 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">{c.label}</p>
|
||||
<c.icon className={`w-4 h-4 ${c.color}`} />
|
||||
</div>
|
||||
<p className={`text-xl font-bold ${c.color}`}>{c.value}</p>
|
||||
{c.sub && <p className="text-xs text-slate-500">{c.sub}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Orders Table ──────────────────────────────────────────────────────────────
|
||||
function OrdersTable({ shop, refreshKey }: { shop: string; refreshKey: number }) {
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => { setPage(1); }, [shop, refreshKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${API_URL}/api/tiktok-order/orders?page=${page}&limit=20&shop=${shop}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setItems(d.items || []); setTotal(d.total || 0); setTotalPages(d.totalPages || 1); })
|
||||
.finally(() => setLoading(false));
|
||||
}, [shop, page, refreshKey]);
|
||||
|
||||
const statusColor = (s: string) => {
|
||||
if (s.includes('เสร็จ')) return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/20';
|
||||
if (s.includes('จัดส่งแล้ว')) return 'bg-blue-500/15 text-blue-400 border-blue-500/20';
|
||||
if (s.includes('จะจัดส่ง') || s.includes('รอ')) return 'bg-amber-500/15 text-amber-400 border-amber-500/20';
|
||||
if (s.includes('ยกเลิก') || s.includes('cancel')) return 'bg-rose-500/15 text-rose-400 border-rose-500/20';
|
||||
return 'bg-white/5 text-slate-400 border-white/10';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-slate-400">ทั้งหมด <span className="text-white font-bold">{total.toLocaleString()}</span> คำสั่งซื้อ</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)} className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"><ChevronLeft className="w-4 h-4" /></button>
|
||||
<span className="text-slate-400 text-xs">{page} / {totalPages}</span>
|
||||
<button disabled={page >= totalPages} onClick={() => setPage(p => p + 1)} className="p-1.5 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"><ChevronRight className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3 whitespace-nowrap">Order ID</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">สถานะ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">ผู้ซื้อ</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">วันที่สร้าง</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap">SKUs</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ยอดรวม (฿)</th>
|
||||
<th className="px-4 py-3 whitespace-nowrap text-right">ค่าส่ง (฿)</th>
|
||||
<th className="px-4 py-3" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={8} className="px-4 py-10 text-center text-slate-500">กำลังโหลด...</td></tr>
|
||||
) : items.length === 0 ? (
|
||||
<tr><td colSpan={8} className="px-4 py-16 text-center text-slate-500">ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน</td></tr>
|
||||
) : items.map((order) => (
|
||||
<tr key={order.orderId} className="border-b border-white/5 hover:bg-white/[0.04] transition-colors group">
|
||||
<td className="px-4 py-3 font-mono text-xs text-rose-400 font-bold">{order.orderId}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium whitespace-nowrap border ${statusColor(order.status)}`}>{order.status}</span>
|
||||
{order.subStatus && <span className="text-[9px] text-slate-500">{order.subStatus}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{order.buyerUsername}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">{order.createdTime?.slice(0, 16)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-white/5 text-slate-400 text-xs border border-white/10">
|
||||
<Package className="w-3 h-3" />{order.items?.length ?? 0} SKU
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono font-bold text-emerald-400">{fmt(order.orderAmount)}</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-amber-400 text-xs">{fmt(order.shippingFee)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<Link to={`/tiktok-order/${encodeURIComponent(order.orderId)}`}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-slate-400 text-xs hover:bg-rose-500/10 hover:border-rose-500/20 hover:text-rose-400 transition-all opacity-0 group-hover:opacity-100">
|
||||
รายละเอียด <ExternalLink className="w-3 h-3" />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Import Panel ──────────────────────────────────────────────────────────────
|
||||
function ImportPanel({ shop, onSuccess }: { shop: string; onSuccess: () => void }) {
|
||||
const [allRows, setAllRows] = useState<any[]>([]);
|
||||
const [previewRows, setPreviewRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||
const [fileInfo, setFileInfo] = useState('');
|
||||
const shopCfg = getShop(shop);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setMsg(null); setAllRows([]); setPreviewRows([]); setFileInfo('');
|
||||
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
delimiter: ',',
|
||||
complete: (result) => {
|
||||
const rows = (result.data as any[]).filter(r => {
|
||||
const id = String(r['Order ID'] ?? '').replace(/\t/g, '').trim();
|
||||
return id.length > 0;
|
||||
});
|
||||
setAllRows(rows);
|
||||
setPreviewRows(rows.slice(0, 5));
|
||||
setFileInfo(`${file.name} | ${rows.length} แถว → ${new Set(rows.map(r => String(r['Order ID']).replace(/\t/g, '').trim())).size} คำสั่งซื้อ`);
|
||||
},
|
||||
error: (err) => setMsg({ text: `อ่านไฟล์ไม่ได้: ${err.message}`, type: 'error' }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!allRows.length) return;
|
||||
setLoading(true); setMsg(null);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/tiktok-order/import`, {
|
||||
method: 'POST', headers: authHeaders(),
|
||||
body: JSON.stringify({ rows: allRows, shop }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setMsg({ text: result.message, type: 'success' });
|
||||
setAllRows([]); setPreviewRows([]); setFileInfo('');
|
||||
const inp = document.getElementById('tiktok-file-input') as HTMLInputElement;
|
||||
if (inp) inp.value = '';
|
||||
onSuccess();
|
||||
} else throw new Error(result.message);
|
||||
} catch (e: any) {
|
||||
setMsg({ text: e.message, type: 'error' });
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleClearData = async () => {
|
||||
if (!confirm(`ยืนยันการลบข้อมูล TikTok ทั้งหมดของร้าน "${shopCfg.label}"?\nการกระทำนี้ไม่สามารถกู้คืนได้`)) return;
|
||||
setClearing(true);
|
||||
setMsg(null);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/tiktok-order/clear?shop=${shop}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders()
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setMsg({ text: result.message, type: 'success' });
|
||||
onSuccess();
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setMsg({ text: e.message, type: 'error' });
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const uniqueOrders = new Set(allRows.map(r => String(r['Order ID']).replace(/\t/g, '').trim())).size;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Target shop indicator */}
|
||||
<div className={`flex items-center gap-3 px-4 py-3 rounded-xl ${shopCfg.bg} border ${shopCfg.border}`}>
|
||||
<Store className={`w-4 h-4 ${shopCfg.color}`} />
|
||||
<p className="text-sm font-medium text-white">
|
||||
Import เข้าร้าน: <span className={`font-bold ${shopCfg.color}`}>{shopCfg.label}</span>
|
||||
</p>
|
||||
<span className={`ml-auto text-xs font-mono ${shopCfg.color} opacity-60`}>{shopCfg.id}</span>
|
||||
</div>
|
||||
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm ${msg.type === 'success' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label htmlFor="tiktok-file-input"
|
||||
className="flex flex-col items-center justify-center border-2 border-dashed border-white/10 hover:border-rose-500/30 rounded-2xl p-12 bg-black/20 cursor-pointer transition-all group gap-4">
|
||||
<div className={`w-16 h-16 rounded-2xl ${shopCfg.bg} border ${shopCfg.border} flex items-center justify-center`}>
|
||||
<Upload className={`w-8 h-8 ${shopCfg.color}`} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white font-bold">คลิกเพื่อเลือกไฟล์ CSV</p>
|
||||
<p className="text-slate-500 text-sm mt-1">รองรับ .csv จาก TikTok Seller Center (Orders Report)</p>
|
||||
</div>
|
||||
<input id="tiktok-file-input" type="file" accept=".csv" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
|
||||
{fileInfo && (
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/10 text-xs font-mono text-slate-400">
|
||||
<span>📋 {fileInfo}</span>
|
||||
<span className={`font-bold ${allRows.length > 0 ? 'text-emerald-400' : 'text-amber-400'}`}>{uniqueOrders} orders</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewRows.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-400 font-medium">Preview (5 แถวแรก):</p>
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-xs text-left">
|
||||
<thead className="bg-white/5 text-slate-400 border-b border-white/10">
|
||||
<tr>
|
||||
{['Order ID', 'Order Status', 'Product Name', 'Variation', 'Quantity', 'SKU Subtotal After Discount', 'Order Amount'].map(k => (
|
||||
<th key={k} className="px-3 py-2 whitespace-nowrap">{k}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{previewRows.map((row, i) => (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/5">
|
||||
{['Order ID', 'Order Status', 'Product Name', 'Variation', 'Quantity', 'SKU Subtotal After Discount', 'Order Amount'].map(k => (
|
||||
<td key={k} className="px-3 py-2 text-slate-300 max-w-[180px] truncate" title={String(row[k] ?? '')}>
|
||||
{String(row[k] ?? '-').replace(/\t/g, '').slice(0, 30)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<button
|
||||
onClick={handleClearData}
|
||||
disabled={clearing}
|
||||
className={`px-6 py-2.5 font-bold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 ${
|
||||
clearing
|
||||
? 'bg-rose-500/10 text-rose-500/50'
|
||||
: 'bg-rose-500/10 text-rose-400 hover:bg-rose-500 hover:text-white border border-rose-500/20'
|
||||
}`}
|
||||
>
|
||||
{clearing ? <><span className="animate-spin">⏳</span> กำลังลบข้อมูล...</> : <><Trash2 className="w-4 h-4" /> ลบข้อมูลทั้งหมดของร้าน {shopCfg.label}</>}
|
||||
</button>
|
||||
|
||||
<button onClick={handleImport} disabled={loading}
|
||||
className={`px-8 py-2.5 font-bold rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 ${shopCfg.activeBg} ${shopCfg.color} border ${shopCfg.activeBorder} hover:opacity-90`}>
|
||||
{loading ? <><span className="animate-spin">⏳</span> กำลัง Import...</> : <><Upload className="w-4 h-4" /> Import {uniqueOrders} คำสั่งซื้อ → {shopCfg.label}</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
type ViewMode = 'list' | 'import';
|
||||
|
||||
export default function TiktokOrderPage() {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [summary, setSummary] = useState<any>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [shop, setShop] = useState('hom-aroma');
|
||||
const shopCfg = getShop(shop);
|
||||
|
||||
const loadSummary = (s: string) => {
|
||||
setSummary(null);
|
||||
fetch(`${API_URL}/api/tiktok-order/summary?shop=${s}`, { headers: authHeaders() })
|
||||
.then(r => r.json()).then(setSummary).catch(() => { });
|
||||
};
|
||||
|
||||
useEffect(() => { loadSummary(shop); }, [shop, refreshKey]);
|
||||
|
||||
const handleShopChange = (s: string) => { setShop(s); setViewMode('list'); };
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl ${shopCfg.bg} border ${shopCfg.border} flex items-center justify-center`}>
|
||||
<ShoppingBag className={`w-5 h-5 ${shopCfg.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">TikTok Orders</h1>
|
||||
<p className={`text-xs uppercase tracking-widest font-bold ${shopCfg.color}`}>{shopCfg.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<ShopSwitcher shop={shop} onChange={handleShopChange} />
|
||||
<div className="h-5 w-px bg-white/10 hidden sm:block" />
|
||||
<button onClick={() => setViewMode('list')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'list' ? 'bg-rose-500/15 text-rose-400 border border-rose-500/30' : 'text-slate-400 hover:text-white bg-white/5 border border-white/10'}`}>
|
||||
<Table2 className="w-4 h-4" /> ดูข้อมูล
|
||||
</button>
|
||||
<button onClick={() => setViewMode('import')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'import' ? `${shopCfg.activeBg} ${shopCfg.color} border ${shopCfg.activeBorder}` : 'text-slate-400 hover:text-white bg-white/5 border border-white/10'}`}>
|
||||
<Upload className="w-4 h-4" /> Import CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SummaryCards summary={summary} shopColor={shopCfg.color} />
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden backdrop-blur-sm">
|
||||
<div className="flex items-center gap-3 px-6 py-4 border-b border-white/10 bg-white/[0.02]">
|
||||
{viewMode === 'list' ? (
|
||||
<><TrendingUp className={`w-4 h-4 ${shopCfg.color}`} /><h2 className="font-bold text-white">คำสั่งซื้อ — {shopCfg.label}</h2></>
|
||||
) : (
|
||||
<><Upload className={`w-4 h-4 ${shopCfg.color}`} /><h2 className="font-bold text-white">นำเข้าข้อมูลจาก TikTok Shop</h2><span className="text-xs text-slate-500">อัพโหลดไฟล์ Orders Report (.csv)</span></>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{viewMode === 'list' ? (
|
||||
<OrdersTable key={`${shop}-${refreshKey}`} shop={shop} refreshKey={refreshKey} />
|
||||
) : (
|
||||
<ImportPanel shop={shop} onSuccess={() => { setRefreshKey(k => k + 1); setViewMode('list'); }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
PieChart, Pie, Cell, Legend,
|
||||
} from 'recharts';
|
||||
import {
|
||||
TrendingUp, DollarSign, Package,
|
||||
BadgePercent, ShoppingCart, Loader2, Store, Calendar,
|
||||
} from 'lucide-react';
|
||||
import { TIKTOK_SHOPS } from './TiktokOrderPage';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
// Color map per shop ID (matches TIKTOK_SHOPS palette)
|
||||
const SHOP_HEX: Record<string, string> = {
|
||||
'hom-aroma': '#fb7185', // rose-400
|
||||
'shop-01': '#a78bfa', // violet-400
|
||||
};
|
||||
const PIE_FALLBACK = ['#22d3ee', '#4ade80', '#fb7185', '#a78bfa'];
|
||||
|
||||
function shopHex(id: string, idx: number) {
|
||||
return SHOP_HEX[id] ?? PIE_FALLBACK[idx % PIE_FALLBACK.length];
|
||||
}
|
||||
function shopLabel(id: string) {
|
||||
return TIKTOK_SHOPS.find(s => s.id === id)?.label ?? id;
|
||||
}
|
||||
|
||||
// ── Date range helpers ────────────────────────────────────────────────────────
|
||||
type Preset = '7d' | '30d' | '90d' | 'thisMonth' | 'lastMonth' | 'year' | 'lastYear' | 'last12m' | 'all';
|
||||
function toISO(d: Date) { return d.toISOString().slice(0, 10); }
|
||||
function getPresetRange(preset: Preset): { from: string; to: string } {
|
||||
const now = new Date();
|
||||
const today = toISO(now);
|
||||
if (preset === '7d') { const f = new Date(now); f.setDate(f.getDate() - 6); return { from: toISO(f), to: today }; }
|
||||
if (preset === '30d') { const f = new Date(now); f.setDate(f.getDate() - 29); return { from: toISO(f), to: today }; }
|
||||
if (preset === '90d') { const f = new Date(now); f.setDate(f.getDate() - 89); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'thisMonth') { const f = new Date(now.getFullYear(), now.getMonth(), 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastMonth') { const f = new Date(now.getFullYear(), now.getMonth() - 1, 1); const t = new Date(now.getFullYear(), now.getMonth(), 0); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'year') { const f = new Date(now.getFullYear(), 0, 1); return { from: toISO(f), to: today }; }
|
||||
if (preset === 'lastYear') { const f = new Date(now.getFullYear() - 1, 0, 1); const t = new Date(now.getFullYear() - 1, 11, 31); return { from: toISO(f), to: toISO(t) }; }
|
||||
if (preset === 'last12m') { const f = new Date(now); f.setFullYear(f.getFullYear() - 1); return { from: toISO(f), to: today }; }
|
||||
return { from: '', to: '' };
|
||||
}
|
||||
const PRESETS: { key: Preset; label: string }[] = [
|
||||
{ key: '7d', label: '7 วัน' },
|
||||
{ key: '30d', label: '30 วัน' },
|
||||
{ key: '90d', label: '90 วัน' },
|
||||
{ key: 'thisMonth', label: 'เดือนนี้' },
|
||||
{ key: 'lastMonth', label: 'เดือนที่แล้ว' },
|
||||
{ key: 'year', label: 'ปีนี้' },
|
||||
{ key: 'lastYear', label: 'ปีที่แล้ว' },
|
||||
{ key: 'last12m', label: '12 เดือนที่ผ่านมา' },
|
||||
{ key: 'all', label: 'ทั้งหมด' },
|
||||
];
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
function fmt(val: number, dec = 0) {
|
||||
return new Intl.NumberFormat('th-TH', { minimumFractionDigits: dec, maximumFractionDigits: dec }).format(val);
|
||||
}
|
||||
function fmtB(val: number) { return `฿${fmt(val, 2)}`; }
|
||||
|
||||
function KPICard({ label, value, sub, color = 'text-white', icon: Icon }: { label: string; value: string; sub?: string; color?: string; icon: any }) {
|
||||
return (
|
||||
<div className="relative bg-white/5 border border-white/10 rounded-2xl p-5 overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-[0.07]"><Icon className="w-14 h-14" /></div>
|
||||
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-1">{label}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{sub && <p className="text-xs text-slate-500 mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="bg-[#0f172a] border border-white/10 rounded-xl px-4 py-3 text-xs shadow-xl space-y-1.5 min-w-[160px]">
|
||||
{label && <p className="text-slate-400 font-bold mb-2">{label}</p>}
|
||||
{payload.map((p: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: p.color }} />
|
||||
<span className="text-slate-400">{p.name}:</span>
|
||||
<span className="font-bold text-white ml-auto">
|
||||
{typeof p.value === 'number' ? (p.value > 1000 ? fmtB(p.value) : fmt(p.value)) : p.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function TiktokOverviewPage() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Date range filter
|
||||
const [preset, setPreset] = useState<Preset>('all');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
|
||||
const loadData = (from: string, to: string) => {
|
||||
setLoading(true); setError(null); setData(null);
|
||||
const params = new URLSearchParams();
|
||||
if (from) params.set('dateFrom', from);
|
||||
if (to) params.set('dateTo', to);
|
||||
const qs = params.toString() ? `?${params}` : '';
|
||||
fetch(`${API_URL}/api/tiktok-order/analytics-overview${qs}`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setData(d); setLoading(false); })
|
||||
.catch(err => { setError(err.message); setLoading(false); });
|
||||
};
|
||||
|
||||
useEffect(() => { loadData('', ''); }, []);
|
||||
|
||||
const applyPreset = (p: Preset) => {
|
||||
setPreset(p);
|
||||
const range = getPresetRange(p);
|
||||
setDateFrom(range.from);
|
||||
setDateTo(range.to);
|
||||
loadData(range.from, range.to);
|
||||
};
|
||||
|
||||
const applyCustom = () => {
|
||||
setPreset('all');
|
||||
loadData(dateFrom, dateTo);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh] gap-3 animate-pulse">
|
||||
<Loader2 className="w-8 h-8 text-rose-400 animate-spin" />
|
||||
<span className="text-slate-500">กำลังรวมข้อมูลทุกร้าน...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data || data.shops.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] gap-4">
|
||||
<Store className="w-14 h-14 text-slate-700" />
|
||||
<p className="text-slate-500">{error || 'ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน'}</p>
|
||||
<Link to="/tiktok-order" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-slate-300 text-sm">
|
||||
← กลับไปหน้า TikTok Orders
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { shops, monthly, topProducts, summary } = data;
|
||||
|
||||
// Pie data for revenue share
|
||||
const pieData = shops.map((s: any) => ({ name: shopLabel(s.shop), value: s.revenue, id: s.shop }));
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in pb-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-rose-500/20 to-violet-500/20 border border-white/10 flex items-center justify-center">
|
||||
<Store className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-0.5">TikTok</p>
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">ภาพรวมทุกร้าน</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<Link
|
||||
key={s.shop}
|
||||
to="/tiktok-analytics"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl border text-xs font-bold transition-all hover:opacity-80"
|
||||
style={{ borderColor: `${shopHex(s.shop, i)}40`, backgroundColor: `${shopHex(s.shop, i)}15`, color: shopHex(s.shop, i) }}
|
||||
>
|
||||
{shopLabel(s.shop)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Date Range Filter Bar ─────────────────────────────────────────── */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Calendar className="w-4 h-4 text-slate-500 mr-1" />
|
||||
{PRESETS.map(p => (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => applyPreset(p.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${
|
||||
preset === p.key
|
||||
? 'bg-rose-500/15 text-rose-400 border-rose-500/30'
|
||||
: 'bg-white/5 text-slate-400 border-white/10 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block h-6 w-px bg-white/10" />
|
||||
|
||||
{/* Custom date range */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-slate-500">กำหนดเอง:</span>
|
||||
<input
|
||||
type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-rose-500/40 focus:outline-none"
|
||||
/>
|
||||
<span className="text-slate-600 text-xs">ถึง</span>
|
||||
<input
|
||||
type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
||||
className="px-3 py-1.5 rounded-lg bg-black/30 border border-white/10 text-slate-300 text-xs focus:border-rose-500/40 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={applyCustom}
|
||||
className="px-4 py-1.5 rounded-lg bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs font-medium hover:bg-rose-500/20 transition-all"
|
||||
>
|
||||
ดูข้อมูล
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active badge + clear */}
|
||||
{(dateFrom || dateTo) && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-500 px-2 py-1 rounded-full border border-white/10 bg-white/[0.02]">
|
||||
{dateFrom || '…'} → {dateTo || '…'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setDateFrom(''); setDateTo(''); setPreset('all'); loadData('', ''); }}
|
||||
className="text-[10px] text-slate-500 hover:text-rose-400 transition-colors"
|
||||
>
|
||||
✕ ล้าง
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grand KPI Row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<KPICard label="ยอดขายรวมทุกร้าน" value={fmtB(summary.totalRevenue)} sub={`${fmt(summary.totalOrders)} คำสั่งซื้อ`} color="text-emerald-400" icon={DollarSign} />
|
||||
<KPICard label="รายได้สุทธิรวม" value={fmtB(summary.netRevenue)} sub={`Margin ${fmt(summary.totalRevenue > 0 ? (summary.netRevenue / summary.totalRevenue) * 100 : 0, 1)}%`} color="text-cyan-glow" icon={TrendingUp} />
|
||||
<KPICard label="ส่วนลด Platform รวม" value={fmtB(summary.totalPlatformDisc)} sub="TikTok อุดหนุนส่วนลด" color="text-purple-400" icon={BadgePercent} />
|
||||
<KPICard label="AOV รวม" value={fmtB(summary.aov)} sub={`ยกเลิก ${fmt(summary.cancelRate, 1)}%`} color="text-amber-400" icon={ShoppingCart} />
|
||||
</div>
|
||||
|
||||
{/* Per-Shop Cards */}
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{shops.map((s: any, i: number) => {
|
||||
const hex = shopHex(s.shop, i);
|
||||
const revShare = summary.totalRevenue > 0 ? (s.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<div
|
||||
key={s.shop}
|
||||
className="bg-white/5 border border-white/10 rounded-2xl p-5 space-y-4"
|
||||
style={{ borderTopColor: hex, borderTopWidth: '2px' }}
|
||||
>
|
||||
{/* Shop header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-bold text-white text-base">{shopLabel(s.shop)}</p>
|
||||
<p className="text-[10px] font-mono text-slate-500">{s.shop}</p>
|
||||
</div>
|
||||
<span className="text-xs font-bold px-2 py-1 rounded-lg" style={{ backgroundColor: `${hex}20`, color: hex }}>
|
||||
{fmt(revShare, 1)}% of total
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
{[
|
||||
{ label: 'ยอดขาย', value: fmtB(s.revenue), color: 'text-emerald-400' },
|
||||
{ label: 'ส่วนลด TikTok', value: fmtB(s.platformDiscount), color: 'text-purple-400' },
|
||||
{ label: 'คำสั่งซื้อ', value: fmt(s.orders), color: 'text-white' },
|
||||
{ label: 'ส่วนลดร้าน', value: fmtB(s.sellerDiscount), color: 'text-rose-400' },
|
||||
{ label: 'ยกเลิก', value: `${fmt(s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0, 1)}%`, color: 'text-amber-400' },
|
||||
{ label: 'ค่าส่งรวม', value: fmtB(s.shippingFee), color: 'text-slate-300' },
|
||||
].map(item => (
|
||||
<div key={item.label}>
|
||||
<p className="text-slate-500 text-[10px] uppercase tracking-wider">{item.label}</p>
|
||||
<p className={`font-bold font-mono ${item.color}`}>{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Revenue bar */}
|
||||
<div>
|
||||
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full transition-all" style={{ width: `${Math.min(revShare, 100)}%`, backgroundColor: hex }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/tiktok-analytics"
|
||||
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-xl text-xs font-medium border transition-all hover:opacity-90"
|
||||
style={{ borderColor: `${hex}30`, backgroundColor: `${hex}10`, color: hex }}
|
||||
>
|
||||
<TrendingUp className="w-3.5 h-3.5" /> ดู Analytics
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Monthly Stacked Revenue Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<TrendingUp className="w-5 h-5 text-rose-400" />
|
||||
<h2 className="text-lg font-bold text-white">ยอดขายรายเดือน (แยกตามร้าน)</h2>
|
||||
</div>
|
||||
{monthly.length > 0 ? (
|
||||
<div className="h-[320px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthly} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} barCategoryGap="30%">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
|
||||
<XAxis dataKey="month" stroke="#64748b" tick={{ fontSize: 11 }} />
|
||||
<YAxis stroke="#64748b" tick={{ fontSize: 11 }} tickFormatter={v => v >= 1000 ? `฿${(v / 1000).toFixed(0)}k` : `฿${v}`} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#ffffff06' }} />
|
||||
<Legend wrapperStyle={{ fontSize: '11px', color: '#94a3b8' }} formatter={(value) => shopLabel(value.replace('_revenue', ''))} />
|
||||
{shops.map((shop: any, i: number) => (
|
||||
<Bar key={shop.shop} dataKey={`${shop.shop}_revenue`} name={`${shop.shop}_revenue`} stackId="rev" fill={shopHex(shop.shop, i)} radius={i === shops.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]} />
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-slate-500 text-center py-10">ยังไม่มีข้อมูลรายเดือน</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Revenue Share Pie + Per-shop comparison table */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Pie Chart */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<BadgePercent className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-lg font-bold text-white">สัดส่วนยอดขายตามร้าน</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-[220px] flex-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie data={pieData} cx="50%" cy="50%" innerRadius={55} outerRadius={90} dataKey="value" nameKey="name" stroke="none" paddingAngle={3}>
|
||||
{pieData.map((entry: any, i: number) => (
|
||||
<Cell key={i} fill={shopHex(entry.id, i)} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', borderRadius: '8px', fontSize: '11px' }}
|
||||
formatter={(v: any) => fmtB(v)} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<div key={s.shop} className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="text-slate-300 font-medium w-24">{shopLabel(s.shop)}</span>
|
||||
<span className="font-mono text-white">{fmtB(s.revenue)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comparison Table */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Store className="w-5 h-5 text-slate-400" />
|
||||
<h2 className="text-lg font-bold text-white">เปรียบเทียบร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left">ร้าน</th>
|
||||
<th className="pb-3 text-right">ยอดขาย</th>
|
||||
<th className="pb-3 text-right">ส่วนลดแพลตฟอร์ม</th>
|
||||
<th className="pb-3 text-right">ออเดอร์</th>
|
||||
<th className="pb-3 text-right">ยกเลิก</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{shops.map((s: any, i: number) => (
|
||||
<tr key={s.shop} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: shopHex(s.shop, i) }} />
|
||||
<span className="font-medium text-white">{shopLabel(s.shop)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 text-right font-mono text-emerald-400 font-bold">{fmtB(s.revenue)}</td>
|
||||
<td className="py-3 text-right font-mono text-purple-400">{fmtB(s.platformDiscount)}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300">{fmt(s.orders)}</td>
|
||||
<td className="py-3 text-right font-mono text-rose-400">{fmt(s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0, 1)}%</td>
|
||||
</tr>
|
||||
))}
|
||||
{/* Total row */}
|
||||
<tr className="bg-white/[0.03]">
|
||||
<td className="py-3 font-bold text-white text-xs">รวมทั้งหมด</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400">{fmtB(summary.totalRevenue)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-purple-400">{fmtB(summary.totalPlatformDisc)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-slate-300">{fmt(summary.totalOrders)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-rose-400">{fmt(summary.cancelRate, 1)}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Products across all shops */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
<h2 className="text-lg font-bold text-white">Top 10 สินค้าขายดี — รวมทุกร้าน</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="text-xs text-slate-500 uppercase border-b border-white/10">
|
||||
<tr>
|
||||
<th className="pb-3 text-left w-8">#</th>
|
||||
<th className="pb-3 text-left">สินค้า</th>
|
||||
<th className="pb-3 text-left">ตัวเลือก</th>
|
||||
<th className="pb-3 text-right">ชิ้น</th>
|
||||
<th className="pb-3 text-right">ยอดรายได้</th>
|
||||
<th className="pb-3 text-right">% รวม</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{topProducts.map((p: any, i: number) => {
|
||||
const pct = summary.totalRevenue > 0 ? (p.revenue / summary.totalRevenue) * 100 : 0;
|
||||
return (
|
||||
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="py-3 text-slate-500 font-bold text-xs">{i + 1}</td>
|
||||
<td className="py-3 text-white font-medium text-xs max-w-[220px] truncate" title={p.name}>{p.name}</td>
|
||||
<td className="py-3 text-slate-400 text-xs">{p.variation || '—'}</td>
|
||||
<td className="py-3 text-right font-mono text-slate-300 text-xs">{fmt(p.qty)}</td>
|
||||
<td className="py-3 text-right font-mono font-bold text-emerald-400 text-xs">{fmtB(p.revenue)}</td>
|
||||
<td className="py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="h-1.5 w-14 rounded-full bg-white/5 overflow-hidden">
|
||||
<div className="h-full rounded-full bg-purple-400/60" style={{ width: `${Math.min(pct * 5, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-[10px] text-slate-400 w-9 text-right">{fmt(pct, 1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
const API_URL = (import.meta.env.VITE_API_URL || 'http://localhost:3000').replace(/\/api\/?$/, '');
|
||||
|
||||
type TabId = 'income' | 'product' | 'product-sku';
|
||||
type ViewMode = 'import' | 'list';
|
||||
type ImportMode = 'file' | 'paste';
|
||||
|
||||
function formatCurrency(val: number) {
|
||||
return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB', maximumFractionDigits: 2 }).format(val);
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('irelax_admin_token')}` };
|
||||
}
|
||||
|
||||
// ─── TSV parser: handles quoted fields (embedded tabs/newlines) + skip rows ────
|
||||
function parseTSVCells(line: string): string[] {
|
||||
const cells: string[] = [];
|
||||
let i = 0;
|
||||
while (i <= line.length) {
|
||||
if (i === line.length) { cells.push(''); break; }
|
||||
if (line[i] === '"') {
|
||||
// Quoted field
|
||||
let val = '';
|
||||
i++; // skip opening quote
|
||||
while (i < line.length) {
|
||||
if (line[i] === '"' && line[i + 1] === '"') { val += '"'; i += 2; }
|
||||
else if (line[i] === '"') { i++; break; }
|
||||
else { val += line[i]; i++; }
|
||||
}
|
||||
cells.push(val.trim());
|
||||
if (line[i] === '\t') i++; // skip tab separator
|
||||
} else {
|
||||
const end = line.indexOf('\t', i);
|
||||
if (end === -1) { cells.push(line.slice(i).trim()); break; }
|
||||
cells.push(line.slice(i, end).trim());
|
||||
i = end + 1;
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
function parseTSV(text: string, skipRows = 0): any[] {
|
||||
// Split lines — handle \r\n and \n
|
||||
const allLines = text.split(/\r?\n/);
|
||||
|
||||
// Find the actual header line (after skipping date-range rows / blank rows)
|
||||
// We use `skipRows` to jump past known non-header rows
|
||||
let headerIdx = 0;
|
||||
let skipped = 0;
|
||||
for (let i = 0; i < allLines.length; i++) {
|
||||
if (skipped >= skipRows) { headerIdx = i; break; }
|
||||
skipped++;
|
||||
}
|
||||
|
||||
const headerLine = allLines[headerIdx];
|
||||
if (!headerLine?.trim()) return [];
|
||||
const headers = parseTSVCells(headerLine).map(h => h.trim());
|
||||
|
||||
const rows: any[] = [];
|
||||
for (let i = headerIdx + 1; i < allLines.length; i++) {
|
||||
const line = allLines[i];
|
||||
if (!line?.trim()) continue;
|
||||
const cells = parseTSVCells(line);
|
||||
// skip rows where the first cell looks like a date range (e.g. "2026-01-01 ~ ...")
|
||||
if (cells[0]?.match(/^\d{4}-\d{2}/)) continue;
|
||||
const row: any = {};
|
||||
headers.forEach((h, idx) => {
|
||||
row[h] = cells[idx] ?? null;
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// ─── Normalize header: strip trailing/leading spaces ─────────────────────────
|
||||
function normalizeHeaders(obj: any): any {
|
||||
const out: any = {};
|
||||
for (const key of Object.keys(obj)) {
|
||||
out[key.trim()] = obj[key];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Summary Cards ────────────────────────────────────────────────────────────
|
||||
function SummaryCards({ summary }: { summary: any }) {
|
||||
if (!summary) return null;
|
||||
const cards = [
|
||||
{ label: 'Income Records', value: summary.incomeCount, sub: null },
|
||||
{ label: 'Total Revenue', value: formatCurrency(summary.totalRevenue), sub: `Settlement: ${formatCurrency(summary.totalSettlement)}` },
|
||||
{ label: 'Products', value: summary.productCount, sub: `SKUs: ${summary.skuCount}` },
|
||||
{ label: 'Total GMV', value: formatCurrency(summary.totalGmv), sub: `Orders: ${summary.totalOrders?.toLocaleString()}` },
|
||||
];
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{cards.map(c => (
|
||||
<div key={c.label} className="bg-white/5 border border-white/10 rounded-2xl p-5">
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-1">{c.label}</p>
|
||||
<p className="text-xl font-bold text-white">{c.value}</p>
|
||||
{c.sub && <p className="text-xs text-slate-500 mt-1">{c.sub}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Data Table ───────────────────────────────────────────────────────────────
|
||||
function DataTable({ tab }: { tab: TabId }) {
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetch(`${API_URL}/api/tiktok/${tab}?page=${page}&limit=20`, { headers: authHeaders() })
|
||||
.then(r => r.json())
|
||||
.then(d => { setItems(d.items || []); setTotal(d.total || 0); setTotalPages(d.totalPages || 1); })
|
||||
.finally(() => setLoading(false));
|
||||
}, [tab, page]);
|
||||
|
||||
const cols: Record<TabId, { key: string; label: string }[]> = {
|
||||
income: [
|
||||
{ key: 'id', label: 'Order ID' },
|
||||
{ key: 'type', label: 'Type' },
|
||||
{ key: 'orderCreatedTime', label: 'Created' },
|
||||
{ key: 'orderSettledTime', label: 'Settled' },
|
||||
{ key: 'currency', label: 'Currency' },
|
||||
{ key: 'totalRevenue', label: 'Revenue (฿)' },
|
||||
{ key: 'totalSettlementAmount', label: 'Settlement (฿)' },
|
||||
{ key: 'totalFees', label: 'Fees (฿)' },
|
||||
],
|
||||
product: [
|
||||
{ key: 'id', label: 'Product ID' },
|
||||
{ key: 'productName', label: 'Product Name' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'gmv', label: 'GMV (฿)' },
|
||||
{ key: 'itemsSold', label: 'Items Sold' },
|
||||
{ key: 'orders', label: 'Orders' },
|
||||
],
|
||||
'product-sku': [
|
||||
{ key: 'id', label: 'SKU ID' },
|
||||
{ key: 'productId', label: 'Product ID' },
|
||||
{ key: 'skuName', label: 'SKU Name' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'gmv', label: 'GMV (฿)' },
|
||||
{ key: 'skuOrders', label: 'Orders' },
|
||||
{ key: 'itemsSold', label: 'Items Sold' },
|
||||
],
|
||||
};
|
||||
|
||||
const numericKeys = new Set(['totalRevenue', 'totalSettlementAmount', 'totalFees', 'gmv']);
|
||||
|
||||
const renderCell = (row: any, key: string) => {
|
||||
const val = row[key];
|
||||
if (numericKeys.has(key)) return <span className="font-mono">{typeof val === 'number' ? val.toFixed(2) : val}</span>;
|
||||
if (key === 'status') return (
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${val === 'Active' ? 'bg-emerald-500/15 text-emerald-400' : 'bg-slate-500/15 text-slate-400'}`}>{val}</span>
|
||||
);
|
||||
return <span className="truncate max-w-[200px] block" title={String(val)}>{String(val ?? '-')}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-slate-400">ทั้งหมด <span className="text-white font-bold">{total.toLocaleString()}</span> รายการ</p>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<button disabled={page <= 1} onClick={() => setPage(p => p - 1)} className="px-3 py-1 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">← ก่อนหน้า</button>
|
||||
<span className="text-slate-400">{page} / {totalPages}</span>
|
||||
<button disabled={page >= totalPages} onClick={() => setPage(p => p + 1)} className="px-3 py-1 rounded-lg bg-white/5 text-slate-300 hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">ถัดไป →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-400 uppercase bg-white/5 border-b border-white/10">
|
||||
<tr>{cols[tab].map(c => <th key={c.key} className="px-4 py-3 whitespace-nowrap">{c.label}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={cols[tab].length} className="px-4 py-10 text-center text-slate-500">กำลังโหลด...</td></tr>
|
||||
) : items.length === 0 ? (
|
||||
<tr><td colSpan={cols[tab].length} className="px-4 py-10 text-center text-slate-500">ยังไม่มีข้อมูล — กรุณา Import ไฟล์ก่อน</td></tr>
|
||||
) : items.map((row, i) => (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/5 transition-colors">
|
||||
{cols[tab].map(c => <td key={c.key} className="px-4 py-3 text-slate-300">{renderCell(row, c.key)}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Import Panel ─────────────────────────────────────────────────────────────
|
||||
function ImportPanel({ tab, onSuccess }: { tab: TabId; onSuccess: () => void }) {
|
||||
const [importMode, setImportMode] = useState<ImportMode>('paste');
|
||||
const [pasteText, setPasteText] = useState('');
|
||||
const [allData, setAllData] = useState<any[]>([]);
|
||||
const [previewData, setPreviewData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [msg, setMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
||||
const [sheetInfo, setSheetInfo] = useState<string>('');
|
||||
const [totalRows, setTotalRows] = useState(0);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const skipRows: Record<TabId, number> = { income: 0, product: 1, 'product-sku': 2 };
|
||||
|
||||
const getSheetName = (wb: XLSX.WorkBook): string => {
|
||||
if (tab === 'income') {
|
||||
return wb.SheetNames.find(n => n.toLowerCase().includes('order')) ?? wb.SheetNames[0];
|
||||
}
|
||||
return wb.SheetNames[0];
|
||||
};
|
||||
|
||||
const parseWorkbook = (wb: XLSX.WorkBook): any[] => {
|
||||
const sheetName = getSheetName(wb);
|
||||
const ws = wb.Sheets[sheetName];
|
||||
const skip = skipRows[tab];
|
||||
const ref = ws['!ref'];
|
||||
console.log(`[XLSX] Sheet: "${sheetName}", ref: ${ref}, skip: ${skip}`);
|
||||
console.log(`[XLSX] All sheets: ${wb.SheetNames.join(', ')}`);
|
||||
setSheetInfo(`อ่าน Sheet: "${sheetName}" (${wb.SheetNames.join(' | ')})`);
|
||||
// raw:true keeps numbers as numbers, header:1 lets us manually handle headers
|
||||
const rawRows = XLSX.utils.sheet_to_json<any[]>(ws, {
|
||||
header: 1,
|
||||
defval: null,
|
||||
blankrows: false,
|
||||
raw: true,
|
||||
}) as any[][];
|
||||
|
||||
// skip rows if needed (product/sku have extra header rows above column names)
|
||||
const headerRowIndex = skip;
|
||||
if (rawRows.length <= headerRowIndex) return [];
|
||||
const headers: string[] = (rawRows[headerRowIndex] as any[]).map((h: any) =>
|
||||
h != null ? String(h).trim() : ''
|
||||
);
|
||||
|
||||
const data: any[] = [];
|
||||
for (let i = headerRowIndex + 1; i < rawRows.length; i++) {
|
||||
const cells = rawRows[i] as any[];
|
||||
// skip truly empty rows
|
||||
if (cells.every(c => c === null || c === undefined || c === '')) continue;
|
||||
const row: any = {};
|
||||
headers.forEach((h, idx) => {
|
||||
row[h] = cells[idx] ?? null;
|
||||
});
|
||||
data.push(row);
|
||||
}
|
||||
|
||||
console.log(`[XLSX] Parsed ${data.length} rows`);
|
||||
if (data.length > 0) {
|
||||
console.log(`[XLSX] First row keys:`, Object.keys(data[0]).slice(0, 10));
|
||||
console.log(`[XLSX] First row sample:`, JSON.stringify(data[0]).slice(0, 300));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const readFileAsWorkbook = (f: File): Promise<XLSX.WorkBook> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = evt => {
|
||||
try {
|
||||
const wb = XLSX.read(evt.target?.result, { type: 'array', cellDates: false });
|
||||
resolve(wb);
|
||||
} catch (e) { reject(e); }
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(f);
|
||||
});
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
setMsg(null); setSheetInfo(''); setAllData([]); setTotalRows(0); setPasteText('');
|
||||
try {
|
||||
const wb = await readFileAsWorkbook(f);
|
||||
const data = parseWorkbook(wb);
|
||||
setAllData(data);
|
||||
setTotalRows(data.length);
|
||||
setPreviewData(data.slice(0, 5));
|
||||
} catch (err: any) {
|
||||
setMsg({ text: `อ่านไฟล์ผิดพลาด: ${err.message}`, type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasteChange = (text: string) => {
|
||||
setPasteText(text);
|
||||
setMsg(null); setAllData([]); setTotalRows(0);
|
||||
if (!text.trim()) { setPreviewData([]); setSheetInfo(''); return; }
|
||||
const skip = skipRows[tab];
|
||||
const data = parseTSV(text, skip).map(normalizeHeaders);
|
||||
console.log(`[Paste] Parsed ${data.length} rows (skip=${skip})`);
|
||||
if (data.length > 0) {
|
||||
console.log(`[Paste] First row keys:`, Object.keys(data[0]).slice(0, 10));
|
||||
console.log(`[Paste] First row sample:`, JSON.stringify(data[0]).slice(0, 300));
|
||||
}
|
||||
setAllData(data);
|
||||
setTotalRows(data.length);
|
||||
setPreviewData(data.slice(0, 5));
|
||||
setSheetInfo(`วางข้อมูลจาก Clipboard`);
|
||||
};
|
||||
|
||||
const toNum = (val: any): number => {
|
||||
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 mapRow = (row: any) => {
|
||||
if (tab === 'income') {
|
||||
// Header keys may have trailing spaces — look up with and without
|
||||
const getId = () => {
|
||||
for (const k of Object.keys(row)) {
|
||||
if (k.replace(/\s+/g, '') === 'Order/adjustmentID') return String(row[k] || '');
|
||||
}
|
||||
return String(Math.random());
|
||||
};
|
||||
const getType = () => {
|
||||
for (const k of Object.keys(row)) {
|
||||
if (k.trim() === 'Type') return String(row[k] || '');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
return {
|
||||
id: getId(),
|
||||
type: getType(),
|
||||
orderCreatedTime: String(row['Order created time'] || ''),
|
||||
orderSettledTime: String(row['Order settled time'] || ''),
|
||||
currency: String(row['Currency'] || ''),
|
||||
totalSettlementAmount: toNum(row['Total settlement amount']),
|
||||
totalRevenue: toNum(row['Total Revenue']),
|
||||
totalFees: toNum(row['Total Fees']),
|
||||
rawData: row,
|
||||
};
|
||||
}
|
||||
if (tab === 'product') return {
|
||||
id: String(row['ID'] || Math.random()),
|
||||
productName: String(row['สินค้า'] || ''),
|
||||
status: String(row['สถานะ'] || ''),
|
||||
gmv: toNum(row['GMV']),
|
||||
itemsSold: toNum(row['รายการที่ขายได้']),
|
||||
orders: toNum(row['คำสั่งซื้อ']),
|
||||
rawData: row,
|
||||
};
|
||||
return {
|
||||
id: String(row['SKU ID'] || Math.random()),
|
||||
productId: String(row['Product ID'] || ''),
|
||||
skuName: String(row['สินค้า'] || ''),
|
||||
status: String(row['สถานะ'] || ''),
|
||||
gmv: toNum(row['GMV']),
|
||||
skuOrders: toNum(row['คำสั่งซื้อ SKU']),
|
||||
itemsSold: toNum(row['รายการที่ขายได้']),
|
||||
rawData: row,
|
||||
};
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (allData.length === 0) return;
|
||||
setLoading(true); setMsg(null);
|
||||
try {
|
||||
const data = allData.map(mapRow).filter(r => r.id && !r.id.startsWith('0.'));
|
||||
console.log(`[Import] Sending ${data.length} rows to backend`);
|
||||
if (data.length > 0) {
|
||||
console.log(`[Import] Sample mapped row:`, JSON.stringify(data[0]).slice(0, 200));
|
||||
}
|
||||
const res = await fetch(`${API_URL}/api/tiktok/import/${tab}`, {
|
||||
method: 'POST', headers: authHeaders(), body: JSON.stringify({ data }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setMsg({ text: result.message, type: 'success' });
|
||||
setPreviewData([]); setPasteText(''); setAllData([]); setTotalRows(0);
|
||||
const inp = document.getElementById('tiktok-file-input') as HTMLInputElement;
|
||||
if (inp) inp.value = '';
|
||||
onSuccess();
|
||||
} else throw new Error(result.message);
|
||||
} catch (e: any) {
|
||||
setMsg({ text: e.message, type: 'error' });
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setPasteText(''); setAllData([]); setPreviewData([]);
|
||||
setTotalRows(0); setSheetInfo(''); setMsg(null);
|
||||
const inp = document.getElementById('tiktok-file-input') as HTMLInputElement;
|
||||
if (inp) inp.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{msg && (
|
||||
<div className={`p-4 rounded-xl text-sm ${msg.type === 'success' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border border-rose-500/20'}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div className="flex gap-2">
|
||||
{(['paste', 'file'] as ImportMode[]).map(m => (
|
||||
<button key={m} onClick={() => { setImportMode(m); reset(); }}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${importMode === m ? 'bg-cyan-glow/15 text-cyan-glow border border-cyan-glow/30' : 'text-slate-400 hover:text-white bg-white/5 border border-white/10'}`}>
|
||||
{m === 'paste' ? '📋 Paste ข้อมูล' : '📄 อัพโหลดไฟล์ Excel'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{importMode === 'paste' ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-slate-500">
|
||||
Copy ข้อมูลจาก TikTok Seller Center → วางที่นี่ (รองรับ Tab-separated / TSV)
|
||||
</p>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={pasteText}
|
||||
onChange={e => handlePasteChange(e.target.value)}
|
||||
placeholder="วางข้อมูล (Ctrl+V) ที่นี่..."
|
||||
rows={8}
|
||||
className="w-full bg-black/30 border border-white/10 rounded-xl px-4 py-3 text-xs text-slate-300 font-mono placeholder:text-slate-600 focus:outline-none focus:border-cyan-glow/40 resize-y"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center border-2 border-dashed border-white/10 rounded-2xl p-10 bg-black/20 gap-3">
|
||||
<p className="text-slate-400 text-sm">เลือกไฟล์ Excel ที่ต้องการ Import</p>
|
||||
<input id="tiktok-file-input" type="file" accept=".xlsx,.xls" onChange={handleFileChange}
|
||||
className="block text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-cyan-glow/10 file:text-cyan-glow hover:file:bg-cyan-glow/20 cursor-pointer" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewData.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{sheetInfo && (
|
||||
<div className="text-xs text-slate-500 bg-white/5 rounded-lg px-3 py-2 font-mono flex items-center gap-2 flex-wrap">
|
||||
<span>📋 {sheetInfo}</span>
|
||||
<span className={`font-bold ${totalRows > 1 ? 'text-emerald-400' : 'text-amber-400'}`}>
|
||||
→ ตรวจพบ {totalRows.toLocaleString()} rows
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-slate-400">Preview (5 แถวแรก) — Mapped fields:</p>
|
||||
<div className="overflow-x-auto border border-white/10 rounded-xl">
|
||||
<table className="w-full text-xs text-left">
|
||||
<thead className="bg-white/5 text-slate-400 border-b border-white/10">
|
||||
<tr>{['id','type','orderCreatedTime','orderSettledTime','currency','totalRevenue','totalSettlementAmount','totalFees'].slice(0, 8).map(k => <th key={k} className="px-3 py-2 whitespace-nowrap">{k}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{previewData.map(mapRow).slice(0, 5).map((row, i) => (
|
||||
<tr key={i} className="border-b border-white/5 hover:bg-white/5">
|
||||
{['id','type','orderCreatedTime','orderSettledTime','currency','totalRevenue','totalSettlementAmount','totalFees'].map(k => <td key={k} className="px-3 py-2 truncate max-w-[140px] text-slate-300">{String((row as any)[k] ?? '-')}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button onClick={handleImport} disabled={loading}
|
||||
className="px-6 py-2 bg-cyan-glow text-black font-semibold rounded-xl hover:bg-cyan-glow/90 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_20px_rgba(34,211,238,0.3)]">
|
||||
{loading ? 'กำลัง Import...' : `Import ${totalRows.toLocaleString()} รายการ เข้าฐานข้อมูล`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||||
export default function TiktokPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('income');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [summary, setSummary] = useState<any>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const loadSummary = () =>
|
||||
fetch(`${API_URL}/api/tiktok/summary`, { headers: authHeaders() })
|
||||
.then(r => r.json()).then(setSummary).catch(() => {});
|
||||
|
||||
useEffect(() => { loadSummary(); }, [refreshKey]);
|
||||
|
||||
const tabs: { id: TabId; label: string }[] = [
|
||||
{ id: 'income', label: 'Income Statement' },
|
||||
{ id: 'product', label: 'Product List' },
|
||||
{ id: 'product-sku', label: 'Product SKU' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center flex-wrap gap-3">
|
||||
<h1 className="text-3xl font-bold text-white tracking-tight">TikTok Data</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => setViewMode('list')}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'list' ? 'bg-cyan-glow/15 text-cyan-glow border border-cyan-glow/30' : 'text-slate-400 hover:text-white bg-white/5'}`}>
|
||||
📊 ดูตาราง
|
||||
</button>
|
||||
<button onClick={() => setViewMode('import')}
|
||||
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${viewMode === 'import' ? 'bg-cyan-glow/15 text-cyan-glow border border-cyan-glow/30' : 'text-slate-400 hover:text-white bg-white/5'}`}>
|
||||
📁 Import ไฟล์
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SummaryCards summary={summary} />
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl overflow-hidden backdrop-blur-sm">
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-white/10">
|
||||
{tabs.map(t => (
|
||||
<button key={t.id} onClick={() => setActiveTab(t.id)}
|
||||
className={`flex-1 py-4 px-6 text-center text-sm font-medium transition-colors ${activeTab === t.id ? 'bg-cyan-glow/10 text-cyan-glow border-b-2 border-cyan-glow' : 'text-slate-400 hover:text-white hover:bg-white/5'}`}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{viewMode === 'list'
|
||||
? <DataTable key={`${activeTab}-${refreshKey}`} tab={activeTab} />
|
||||
: <ImportPanel tab={activeTab} onSuccess={() => { setRefreshKey(k => k + 1); setViewMode('list'); }} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
Order ID,Order Status,Order Substatus,Cancelation/Return Type,Normal or Pre-order,SKU ID,Seller SKU,Product Name,Variation,Quantity,Sku Quantity of return,SKU Unit Original Price,SKU Subtotal Before Discount,SKU Platform Discount,SKU Seller Discount,SKU Subtotal After Discount,Shipping Fee After Discount,Original Shipping Fee,Shipping Fee Seller Discount,Shipping Fee Platform Discount,Payment platform discount,Taxes,Order Amount,Order Refund Amount,Created Time,Paid Time,RTS Time,Shipped Time,Delivered Time,Cancelled Time,Cancel By,Cancel Reason,Fulfillment Type,Warehouse Name,Tracking ID,Delivery Option,Shipping Provider Name,Buyer Message,Buyer Username,Recipient,Phone #,Zipcode,Country,Province,District,Districts,Detail Address,Additional address information,Payment Method,Weight(kg),Product Category,Package ID,Seller Note,Checked Status,Checked Marked by,Request Tax Invoice,Tax Info - Buyer Tax ID,Tax Info - Type,Tax Info - Full Name of Buyer,Tax Info - Email,Tax Info - Phone Number,Tax Info - Registered Address,Tax Info - Address Type
|
||||
583701253535401517 ,ที่จะจัดส่ง,รอเข้ารับ,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,10,140,0,46,0,46,,0,140,,26/04/2026 09:01:50 ," ",26/04/2026 09:19:40 ," "," "," ",,,Fulfillment by seller,คลังหลัก,795669200056,การจัดส่งแบบมาตรฐาน,J&T Express,,u***8387181348271,ส******* แ*****,(+66)081*****87,***** ,ไทย,ระยอง,เมืองระยอง,,************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1194140166046320173 ,,Unchecked,,,,,,,,,
|
||||
583698482293081664 ,ที่จะจัดส่ง,รอเข้ารับ,,Normal,1732134722810579316 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น นมสด สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,10,69,0,38,0,38,,0,69,,26/04/2026 00:50:59 ," ",26/04/2026 08:10:50 ," "," "," ",,,Fulfillment by seller,คลังหลัก,795639041046,การจัดส่งแบบมาตรฐาน,J&T Express,,s***thaibarbershop,ศ*********** ศ*******,(+66)649****58,***** ,ไทย,สุพรรณบุรี,เมืองสุพรรณบุรี,,*********************************************************************************,,การเก็บเงินปลายทาง,0.1,น้ำมันหอมระเหย,1194121959917520448 ,,Unchecked,,,,,,,,,
|
||||
583687621436343794 ,จัดส่งแล้ว,อยู่ระหว่างขนส่ง,,Normal,1731406465435600244 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกแก้ว,1,0,150,150,0,10,140,0,35,0,35,,0,140,,25/04/2026 11:07:14 ," ",25/04/2026 11:57:33 ,25/04/2026 12:18:14 ," "," ",,,Fulfillment by seller,คลังหลัก,795680001756,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5669817104390,ร********* บ*******,(+66)812****37,***** ,ไทย,กรุงเทพฯ,บางกะปิ,,************,*****,การเก็บเงินปลายทาง,0.1,สเปรย์,1194050846089709042 ,,Unchecked,,,,,,,,,
|
||||
583686010523584104 ,จัดส่งแล้ว,อยู่ระหว่างขนส่ง,,Normal,1732134145388546420 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น โอเชี่ยน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,2,0,79,158,0,10,148,38,38,0,0,,0,186,,25/04/2026 09:09:42 ,25/04/2026 09:10:06 ,25/04/2026 09:24:09 ,25/04/2026 12:18:14 ," "," ",,,Fulfillment by seller,คลังหลัก,795677388505,การจัดส่งแบบมาตรฐาน,J&T Express,,m***12345.lindgren,โ***************************************,(+66)821****28,***** ,ไทย,ชลบุรี,บางละมุง,,***************,*************************************************************************************************************************,Mbanking,0.2,สเปรย์,1194036569562056296 ,,Unchecked,,,,,,,,,
|
||||
583673419146429680 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,7,10,133,29,29,0,0,,0,162,,24/04/2026 12:30:59 ,25/04/2026 16:43:18 ,24/04/2026 12:58:48 ,24/04/2026 13:12:06 ,25/04/2026 16:43:18 ," ",,,Fulfillment by seller,คลังหลัก,795620940155,การจัดส่งแบบมาตรฐาน,J&T Express,,t***yarat28,ธ********* ห*****,(+66)923****22,***** ,ไทย,กาญจนบุรี,พนมทวน,,************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193946422385804528 ,,Unchecked,,,,,,,,,
|
||||
583671281087513854 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732125161763998068 ,,Hom Aroma น้ำมันหอมระเหย กลิ่นวานิลลา สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,0,130,130,0,10,120,38,38,0,0,,0,158,,24/04/2026 10:01:52 ,25/04/2026 14:02:43 ,24/04/2026 12:57:32 ,24/04/2026 13:12:07 ,25/04/2026 14:02:43 ," ",,,Fulfillment by seller,คลังหลัก,795636141134,การจัดส่งแบบมาตรฐาน,J&T Express,,u***nznwozj02q,ไ******* ส****,(+66)062*****09,***** ,ไทย,Chon Buri,Bo Thong,,***************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193929418926884094 ,,Unchecked,,,,,,,,,
|
||||
583670847078237562 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,9.83,140.17,0,35,0,35,,0,214,,24/04/2026 09:26:21 ,25/04/2026 12:30:41 ,24/04/2026 12:55:44 ,24/04/2026 13:12:07 ,25/04/2026 12:30:41 ," ",,,Fulfillment by seller,คลังหลัก,795650348124,การจัดส่งแบบมาตรฐาน,J&T Express,,s***pat2020,พ******,(+66)619****65,***** ,ไทย,สมุทรปราการ,บางเสาธง,,********************************************,*************,การเก็บเงินปลายทาง,0.1,สเปรย์,1193924401030858106 ,,Unchecked,,,,,,,,,
|
||||
583670847078237562 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,79,79,0,5.17,73.83,0,35,0,35,,0,214,,24/04/2026 09:26:21 ,25/04/2026 12:30:41 ,24/04/2026 12:55:44 ,24/04/2026 13:12:07 ,25/04/2026 12:30:41 ," ",,,Fulfillment by seller,คลังหลัก,795650348124,การจัดส่งแบบมาตรฐาน,J&T Express,,s***pat2020,พ******,(+66)619****65,***** ,ไทย,สมุทรปราการ,บางเสาธง,,********************************************,*************,การเก็บเงินปลายทาง,0.1,สเปรย์,1193924401030858106 ,,Unchecked,,,,,,,,,
|
||||
583665968886416755 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732333334796470644 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,17.5,10,157.5,0,35,0,35,,0,157.5,,23/04/2026 22:13:23 ,23/04/2026 22:13:27 ,24/04/2026 07:33:41 ,24/04/2026 08:40:07 ,25/04/2026 17:10:25 ," ",,,Fulfillment by seller,คลังหลัก,795534848846,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ater123,ธ*** เ***,(+66)098*****29,***** ,ไทย,สมุทรปราการ,บางพลี,,*****************************************************************************************,,บัตรเครดิต/เดบิต,0.12,สเปรย์,1193889578413622643 ,,Unchecked,,,,,,,,,
|
||||
583665603838837976 ,จัดส่งแล้ว,อยู่ระหว่างขนส่ง,,Normal,1732133968855861620 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น แคนตาลูป สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,10,170,0,38,0,38,,0,170,,23/04/2026 21:49:26 ," ",24/04/2026 07:36:58 ,24/04/2026 08:40:11 ," "," ",,,Fulfillment by seller,คลังหลัก,795522444861,การจัดส่งแบบมาตรฐาน,J&T Express,,e***mu,ช*** ส*****,(+66)064*****95,***** ,ไทย,ศรีสะเกษ,กันทรารมย์,,**************************************************,**************,การเก็บเงินปลายทาง,0.1,น้ำมันหอมระเหย,1193888757853619416 ,,Unchecked,,,,,,,,,
|
||||
583661489812899539 ,จัดส่งแล้ว,อยู่ระหว่างขนส่ง,,Normal,1731406465435600244 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกแก้ว,1,0,150,150,0,7.5,142.5,0,35,0,35,,0,285,,23/04/2026 16:49:21 ," ",23/04/2026 16:56:06 ,24/04/2026 08:40:08 ," "," ",,,Fulfillment by seller,คลังหลัก,795526351690,การจัดส่งแบบมาตรฐาน,J&T Express,,j***oy_0413,ร****,(+66)829****19,***** ,ไทย,สมุทรปราการ,บางเสาธง,,********************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193863997444425427 ,,Unchecked,,,,,,,,,
|
||||
583661489812899539 ,จัดส่งแล้ว,อยู่ระหว่างขนส่ง,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,0,7.5,142.5,0,35,0,35,,0,285,,23/04/2026 16:49:21 ," ",23/04/2026 16:56:06 ,24/04/2026 08:40:08 ," "," ",,,Fulfillment by seller,คลังหลัก,795526351690,การจัดส่งแบบมาตรฐาน,J&T Express,,j***oy_0413,ร****,(+66)829****19,***** ,ไทย,สมุทรปราการ,บางเสาธง,,********************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193863997444425427 ,,Unchecked,,,,,,,,,
|
||||
583654121501656182 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,150,,23/04/2026 06:49:55 ,25/04/2026 12:39:34 ,23/04/2026 07:15:58 ,23/04/2026 10:21:50 ,25/04/2026 12:39:34 ," ",,,Fulfillment by seller,คลังหลัก,795547030246,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rudee,ส***** ,(+66)844****46,***** ,ไทย,นครศรีธรรมราช,นครศรีธรรมราช,,**************************************************,*********,การเก็บเงินปลายทาง,0.1,สเปรย์,1193795016714060918 ,,Unchecked,,,,,,,,,
|
||||
583652785959241683 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732125161763932532 ,,Hom Aroma น้ำมันหอมระเหย กลิ่นวานิลลา สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,10,69,0,35,0,35,,0,69,,23/04/2026 01:28:00 ,23/04/2026 01:28:15 ,23/04/2026 07:14:36 ,23/04/2026 10:21:50 ,24/04/2026 11:12:20 ," ",,,Fulfillment by seller,คลังหลัก,795527431230,การจัดส่งแบบมาตรฐาน,J&T Express,,k***yzomzom,Z*****,(+66)815****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,************************,******************************************************************,Mbanking,0.1,สเปรย์,1193785899353475027 ,,Unchecked,,,,,,,,,
|
||||
583649695871698352 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732333350293637492 ,P-SKU-OZO-RED-APPLE,น้ำมันหอมไอน้ำ กลิ่น แอปเปิ้ลแดง ขนาด 85ml กลิ่นหอมติดทนนาน,60 ml,1,0,150,150,0,0,150,0,11,0,11,,0,150,,22/04/2026 20:41:57 ,24/04/2026 16:43:14 ,23/04/2026 07:11:20 ,23/04/2026 10:21:50 ,24/04/2026 16:43:14 ," ",,,Fulfillment by seller,คลังหลัก,795533919272,การจัดส่งแบบมาตรฐาน,J&T Express,,u***8009448118582,พ******,(+66)612****63,***** ,ไทย,ชลบุรี,สัตหีบ,,*****************************************************,,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1193771212046108080 ,,Unchecked,,,,,,,,,
|
||||
583649580749456816 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732333356117231988 ,P-SKU-OZO-GREEN-APPLE,aroma oil กลิ่น แอปเปิ้ลเขียว ขนาดมาตรฐาน ใช้ได้นาน เหมาะสำหรับคนแพ้ง่าย,60 ml,1,0,150,150,0,10,140,0,11,0,11,,0,140,,22/04/2026 20:39:26 ,24/04/2026 16:43:14 ,23/04/2026 07:10:16 ,23/04/2026 10:21:50 ,24/04/2026 16:43:14 ," ",,,Fulfillment by seller,คลังหลัก,795582516260,การจัดส่งแบบมาตรฐาน,J&T Express,,u***8009448118582,พ******,(+66)612****63,***** ,ไทย,ชลบุรี,สัตหีบ,,*****************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1193771735663347120 ,,Unchecked,,,,,,,,,
|
||||
583645858410824901 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732333334796470644 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,0,10,175,0,35,0,35,,0,175,,22/04/2026 15:58:45 ,22/04/2026 15:58:48 ,22/04/2026 16:18:19 ,23/04/2026 10:21:51 ,24/04/2026 12:14:16 ," ",,,Fulfillment by seller,คลังหลัก,795421470056,การจัดส่งแบบมาตรฐาน,J&T Express,,f***x8888,อ****** เ**************,(+66)864****94,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตธนบุรี,,**********************************************,,บัตรเครดิต/เดบิต,0.12,สเปรย์,1193744271166178501 ,,Unchecked,,,,,,,,,
|
||||
583636095139874466 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732134700143642996 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกชมนาด สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,10,69,0,35,0,35,,0,69,,21/04/2026 22:21:04 ,21/04/2026 22:21:48 ,22/04/2026 07:04:04 ,22/04/2026 09:19:17 ,23/04/2026 10:56:23 ," ",,,Fulfillment by seller,คลังหลัก,795449422644,การจัดส่งแบบมาตรฐาน,J&T Express,,k***phanchannel,น********* ก******,(+66)840****88,***** ,ราชอาณาจักรไทย,ปทุมธานี,ธัญบุรี,,*******************************************************,,Mbanking,0.1,สเปรย์,1193663667310724770 ,,Unchecked,,,,,,,,,
|
||||
583635771413923267 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732134435908389236 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,0,130,130,0,10,120,0,35,0,35,,0,120,,21/04/2026 22:02:10 ,21/04/2026 22:02:37 ,22/04/2026 07:01:29 ,22/04/2026 09:19:17 ,23/04/2026 13:00:49 ," ",,,Fulfillment by seller,คลังหลัก,795441624633,การจัดส่งแบบมาตรฐาน,J&T Express,,t***..story,ภ**** ร*******,(+66)083*****37,***** ,ไทย,สมุทรปราการ,บางบ่อ,,**********************************************,************,Mbanking,0.1,น้ำมันหอมระเหย,1193662560533710275 ,,Unchecked,,,,,,,,,
|
||||
583626302530422073 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732134685105686900 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กาแฟ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,21/04/2026 10:20:28 ,23/04/2026 13:31:17 ,21/04/2026 12:47:45 ,22/04/2026 09:19:18 ,23/04/2026 13:31:17 ," ",,,Fulfillment by seller,คลังหลัก,795422455310,การจัดส่งแบบมาตรฐาน,J&T Express,,n***ukeyb,บ****** น*******,(+66)081*****05,***** ,ราชอาณาจักรไทย,สมุทรปราการ,เมืองสมุทรปราการ,,********************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193593026461730105 ,,Unchecked,,,,,,,,,
|
||||
583611919843821423 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,10,140,0,35,0,35,,0,140,,20/04/2026 10:55:02 ,22/04/2026 11:41:54 ,20/04/2026 12:24:27 ,21/04/2026 10:35:13 ,22/04/2026 11:41:54 ," ",,,Fulfillment by seller,คลังหลัก,795365426704,การจัดส่งแบบมาตรฐาน,J&T Express,,n***a.spa8,N**** s**,(+66)943****68,***** ,ไทย,ปทุมธานี,ธัญบุรี,,******************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193480921321342831 ,,Unchecked,,,,,,,,,
|
||||
583601523903595758 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,12,0,138,8,88,0,80,,0,146,,19/04/2026 16:41:25 ,22/04/2026 18:21:48 ,19/04/2026 16:50:45 ,20/04/2026 08:39:11 ,22/04/2026 18:21:48 ," ",,,Fulfillment by seller,คลังหลัก,795337514156,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193406943153194222 ,,Unchecked,,,,,,,,,
|
||||
583598595196159283 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435862388 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะนาว,1,0,150,150,0,10,140,0,0,0,0,,0,140,,19/04/2026 12:51:52 ,21/04/2026 21:49:47 ,19/04/2026 16:02:30 ,20/04/2026 08:39:11 ,21/04/2026 21:49:47 ," ",,,Fulfillment by seller,คลังหลัก,795314108105,การจัดส่งแบบมาตรฐาน,J&T Express,,u***8870498353981,ธ******** ช**,(+66)066*****97,***** ,ไทย,กรุงเทพฯ,ดินแดง,,******************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193384336612558131 ,,Unchecked,,,,,,,,,
|
||||
583589047332996173 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732133968855861620 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น แคนตาลูป สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,40.34,10,129.66,0,35,0,35,,0,129.66,,18/04/2026 19:37:55 ,18/04/2026 19:37:58 ,19/04/2026 08:18:36 ,19/04/2026 09:26:26 ,20/04/2026 14:07:12 ," ",,,Fulfillment by seller,คลังหลัก,795291758855,การจัดส่งแบบมาตรฐาน,J&T Express,,w***th.fortune.grow,ก**** ล******,(+66)094*****99,***** ,ไทย,สมุทรปราการ,เมืองสมุทรปราการ,,*************************************************************************************************,,บัตรเครดิต/เดบิต,0.1,น้ำมันหอมระเหย,1193327230681711693 ,,Unchecked,,,,,,,,,
|
||||
583586202458687209 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,10,140,0,38,0,38,,0,140,,18/04/2026 15:53:36 ,21/04/2026 13:57:44 ,18/04/2026 16:41:20 ,19/04/2026 09:26:25 ,21/04/2026 13:57:44 ," ",,,Fulfillment by seller,คลังหลัก,795244887662,การจัดส่งแบบมาตรฐาน,J&T Express,,g***y177,ร******** พ******,(+66)885****28,***** ,ไทย,สกลนคร,เมืองสกลนคร,,******************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193307914130523881 ,,Unchecked,,,,,,,,,
|
||||
583583848197096945 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,29,0,29,,0,150,,18/04/2026 12:45:21 ,20/04/2026 19:16:08 ,18/04/2026 16:41:00 ,19/04/2026 09:26:25 ,20/04/2026 19:16:08 ," ",,,Fulfillment by seller,คลังหลัก,795245688661,การจัดส่งแบบมาตรฐาน,J&T Express,,w***ya27,w*******,(+66)631****99,***** ,ไทย,ประจวบคีรีขันธ์,หัวหิน,,***************************************************************************,*****,การเก็บเงินปลายทาง,0.1,สเปรย์,1193286581890287089 ,,Unchecked,,,,,,,,,
|
||||
583574365228401807 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435534708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,150,150,8.77,3.75,137.48,0,35,0,35,,0,549.9,,17/04/2026 19:40:06 ,19/04/2026 13:08:12 ,18/04/2026 07:08:26 ,18/04/2026 11:14:48 ,19/04/2026 13:08:12 ," ",,,Fulfillment by seller,คลังหลัก,795278062250,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5348668156918,L**,(+66)928****69,***** ,ไทย,ปทุมธานี,คลองหลวง,,***********************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193218748894577807 ,,Unchecked,,,,,,,,,
|
||||
583574365228401807 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465436190068 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โรสเมรี่,1,0,150,150,8.78,3.75,137.47,0,35,0,35,,0,549.9,,17/04/2026 19:40:06 ,19/04/2026 13:08:12 ,18/04/2026 07:08:26 ,18/04/2026 11:14:48 ,19/04/2026 13:08:12 ," ",,,Fulfillment by seller,คลังหลัก,795278062250,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5348668156918,L**,(+66)928****69,***** ,ไทย,ปทุมธานี,คลองหลวง,,***********************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193218748894577807 ,,Unchecked,,,,,,,,,
|
||||
583574365228401807 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406473357395316 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอโซน,1,0,150,150,8.77,3.75,137.48,0,35,0,35,,0,549.9,,17/04/2026 19:40:06 ,19/04/2026 13:08:12 ,18/04/2026 07:08:26 ,18/04/2026 11:14:48 ,19/04/2026 13:08:12 ," ",,,Fulfillment by seller,คลังหลัก,795278062250,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5348668156918,L**,(+66)928****69,***** ,ไทย,ปทุมธานี,คลองหลวง,,***********************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193218748894577807 ,,Unchecked,,,,,,,,,
|
||||
583574365228401807 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,8.78,3.75,137.47,0,35,0,35,,0,549.9,,17/04/2026 19:40:06 ,19/04/2026 13:08:12 ,18/04/2026 07:08:26 ,18/04/2026 11:14:48 ,19/04/2026 13:08:12 ," ",,,Fulfillment by seller,คลังหลัก,795278062250,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5348668156918,L**,(+66)928****69,***** ,ไทย,ปทุมธานี,คลองหลวง,,***********************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1193218748894577807 ,,Unchecked,,,,,,,,,
|
||||
583573079228909176 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1732333353125972340 ,P-SKU-OZO-POMEGRANATE,น้ำมันอโรม่า ขนาดมาตรฐาน ใช้ได้นาน กลิ่น ทับทิม กลิ่นหอมธรรมชาติ,30 ml,1,0,79,79,0,10,69,0,38,0,38,,0,69,,17/04/2026 18:02:45 ,17/04/2026 18:02:47 ,17/04/2026 19:25:49 ,18/04/2026 11:14:48 ,20/04/2026 13:37:29 ," ",,,Fulfillment by seller,คลังหลัก,795209845102,การจัดส่งแบบมาตรฐาน,J&T Express,,m***charinthip,ช*********** ป*****,(+66)097*****92,***** ,ไทย,เชียงใหม่,สันทราย,,*********************************************************************,,PayLater,0.12,น้ำมันหอมระเหย,1193210186721887864 ,,Unchecked,,,,,,,,,
|
||||
583559373429244929 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,3,0,150,450,0,15,435,0,40,0,40,,0,435,,16/04/2026 19:19:32 ,19/04/2026 15:22:47 ,17/04/2026 06:04:34 ,17/04/2026 08:35:13 ,19/04/2026 15:22:47 ," ",,,Fulfillment by seller,คลังหลัก,795102354616,การจัดส่งแบบมาตรฐาน,J&T Express,,n***824,ป****** น*************,(+66)093*****09,***** ,ไทย,อุบลราชธานี,นาจะหลวย,,*********************************,************************,การเก็บเงินปลายทาง,0.3,สเปรย์,1193104979461112833 ,,Unchecked,,,,,,,,,
|
||||
583539630006699020 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249381236 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,15/04/2026 11:32:50 ,17/04/2026 14:14:28 ,15/04/2026 17:02:25 ,16/04/2026 08:44:55 ,17/04/2026 14:14:28 ," ",,,Fulfillment by seller,คลังหลัก,795030543882,การจัดส่งแบบมาตรฐาน,J&T Express,,t***hotencho8,T** c**,(+66)061*****49,***** ,タイ,バンコク,クローントゥーイ,,*******************,*******,การเก็บเงินปลายทาง,0.1,สเปรย์,1192949022247453708 ,,Unchecked,,,,,,,,,
|
||||
583539641775195148 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169504619137396 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,10,190,0,35,0,35,,0,190,,15/04/2026 11:32:36 ,17/04/2026 14:14:28 ,15/04/2026 17:03:06 ,16/04/2026 08:44:55 ,17/04/2026 14:14:28 ," ",,,Fulfillment by seller,คลังหลัก,795012545880,การจัดส่งแบบมาตรฐาน,J&T Express,,t***hotencho8,T** c**,(+66)061*****49,***** ,タイ,バンコク,クローントゥーイ,,*******************,*******,การเก็บเงินปลายทาง,0.1,สเปรย์,1192948718442087436 ,,Unchecked,,,,,,,,,
|
||||
583529532822750213 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,10,190,0,88,0,88,,0,190,,14/04/2026 16:42:32 ,18/04/2026 19:39:00 ,14/04/2026 19:07:39 ,15/04/2026 09:07:59 ,18/04/2026 19:39:00 ," ",,,Fulfillment by seller,คลังหลัก,795003094480,การจัดส่งแบบมาตรฐาน,J&T Express,,p***y_05,ป******,(+66)082*****73,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,***************,****************,การเก็บเงินปลายทาง,0.1,สเปรย์,1192878618393609221 ,,Unchecked,,,,,,,,,
|
||||
583528073452487851 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333417262385524 ,P-SKU-OZO-ORANGE-JASMINE,อโรม่าหอมระเหย ขนาด 85ml กลิ่น ดอกแก้ว กลิ่นหอมสดชื่น,120 ml,1,0,185,185,0,10,175,0,38,0,38,,0,175,,14/04/2026 14:37:05 ,18/04/2026 17:28:04 ,14/04/2026 19:06:45 ,15/04/2026 09:07:58 ,18/04/2026 17:28:04 ," ",,,Fulfillment by seller,คลังหลัก,795040299480,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7624252812281,บ**** บ******* 1* 1* ห*** 1* ต********* อ********* จ******* 2**,(+66)949****23,***** ,ไทย,ชลบุรี,บางละมุง,,*************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1192865981619864747 ,,Unchecked,,,,,,,,,
|
||||
583525433026381038 ,จัดส่งแล้ว,จัดส่งสำเร็จ,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,8,88,0,80,,0,158,,14/04/2026 11:04:25 ,20/04/2026 11:00:04 ,14/04/2026 19:05:39 ,15/04/2026 09:07:58 ,20/04/2026 11:00:04 ," ",,,Fulfillment by seller,คลังหลัก,795069987412,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1192843380070647022 ,,Unchecked,,,,,,,,,
|
||||
583482106790184806 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,150,,11/04/2026 08:50:11 ,16/04/2026 13:28:04 ,11/04/2026 10:30:52 ,11/04/2026 12:09:37 ,16/04/2026 13:28:04 ," ",,,Fulfillment by seller,คลังหลัก,794966941462,การจัดส่งแบบมาตรฐาน,J&T Express,,s***nthasrisangwa,ส******* ศ**********,(+66)082*****54,***** ,ไทย,ปราจีนบุรี,นาดี,,*********************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1192528530324752230 ,,Unchecked,,,,,,,,,
|
||||
583465944860624144 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249381236 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,3,0,180,540,0,0,540,0,38,0,38,,0,540,,10/04/2026 05:34:13 ,13/04/2026 18:00:44 ,10/04/2026 06:38:59 ,10/04/2026 08:55:04 ,13/04/2026 18:00:44 ," ",,,Fulfillment by seller,คลังหลัก,794819417722,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1920702230328,u****************,(+66)814****07,***** ,ไทย,กาฬสินธุ์,ยางตลาด,,***************************************************************,*******************************************************************,การเก็บเงินปลายทาง,0.3,สเปรย์,1192401956694295824 ,,Unchecked,,,,,,,,,
|
||||
583459146942285739 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357919604 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกลำดวน,1,0,150,150,0,0,150,0,38,0,38,,0,150,,09/04/2026 17:18:41 ,12/04/2026 17:54:33 ,09/04/2026 17:44:03 ,10/04/2026 08:55:04 ,12/04/2026 17:54:33 ," ",,,Fulfillment by seller,คลังหลัก,794847475564,การจัดส่งแบบมาตรฐาน,J&T Express,,p***thongsuk,ร***************** ร************************* ร****************,(+66)099*****19,***** ,ไทย,ขอนแก่น,หนองเรือ,,****************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1192365064666056619 ,,Unchecked,,,,,,,,,
|
||||
583439263599985902 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,1,150,150,0,0,150,8,88,0,80,,0,158,150,08/04/2026 11:10:25 ," ",08/04/2026 11:14:23 ,08/04/2026 12:48:01 ," ",16/04/2026 12:58:08 ,System,การจัดส่งพัสดุไม่สำเร็จ,Fulfillment by seller,คลังหลัก,794712954733,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1192209481200207086 ,,Unchecked,,,,,,,,,
|
||||
583426862251083149 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,200,400,0,0,400,0,35,0,35,,0,400,,07/04/2026 14:16:37 ,07/04/2026 14:16:54 ,07/04/2026 16:39:00 ,08/04/2026 08:50:21 ,10/04/2026 11:59:40 ," ",,,Fulfillment by seller,คลังหลัก,794772415345,การจัดส่งแบบมาตรฐาน,J&T Express,,t***.taka5,T******** Y****** ,(+66)859****00,***** ,タイ,Bangkok,クローントゥーイ,,****************************************************************************************,,Mbanking,0.2,สเปรย์,1192115912308458893 ,,Unchecked,,,,,,,,,
|
||||
583423755590535081 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,0,88,0,88,,0,150,,07/04/2026 10:25:30 ,10/04/2026 21:50:39 ,07/04/2026 16:38:06 ,08/04/2026 08:50:22 ,10/04/2026 21:50:39 ," ",,,Fulfillment by seller,คลังหลัก,794710814343,การจัดส่งแบบมาตรฐาน,J&T Express,,u***118054170769,จ***** ป*******,(+66)081*****04,***** ,ไทย,กระบี่,เมืองกระบี่,,********************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1192088746577790889 ,,Unchecked,,,,,,,,,
|
||||
583407462679807706 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333386395650420 ,P-SKU-OZO-COFFEE,น้ำมันหอมอโรมา กลิ่น กาแฟ ขนาด 120ml ใช้ในบ้านและรถยนต์,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,06/04/2026 08:44:25 ,08/04/2026 12:20:32 ,06/04/2026 11:20:10 ,07/04/2026 09:48:53 ,08/04/2026 12:20:32 ," ",,,Fulfillment by seller,คลังหลัก,794641831480,การจัดส่งแบบมาตรฐาน,J&T Express,,u***530615038,ส*** น*******,(+66)093*****88,***** ,ไทย,นนทบุรี,บางบัวทอง,,**************************************************************************************,**************,การเก็บเงินปลายทาง,0.12,สเปรย์,1191955129141593818 ,,Unchecked,,,,,,,,,
|
||||
583403119112783190 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435338100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,1,150,150,0,0,150,0,24,0,24,,0,150,150,05/04/2026 21:59:36 ," "," "," "," ",05/04/2026 23:13:24 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,s***tpotisan,ส****** โ******,(+66)852****95,***** ,ไทย,สมุทรปราการ,บางพลี,,****************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191922043551647062 ,,Unchecked,,,,,,,,,
|
||||
583385512191952235 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,35,0,35,,0,150,,04/04/2026 19:33:33 ,06/04/2026 13:33:56 ,04/04/2026 20:31:19 ,05/04/2026 12:43:16 ,06/04/2026 13:33:56 ," ",,,Fulfillment by seller,คลังหลัก,794512259376,การจัดส่งแบบมาตรฐาน,J&T Express,,n***4114,น***** น***,(+66)092*****77,***** ,ไทย,กรุงเทพมหานคร,เขตบางรัก,,*******************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191814238833706347 ,,Unchecked,,,,,,,,,
|
||||
583381817056331550 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435927924 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แคนตาลูป,1,0,150,150,9.43,0,140.57,0,35,0,35,,0,140.57,,04/04/2026 14:58:50 ,04/04/2026 14:58:52 ,04/04/2026 15:22:57 ,05/04/2026 12:43:16 ,06/04/2026 19:14:56 ," ",,,Fulfillment by seller,คลังหลัก,794591157253,การจัดส่งแบบมาตรฐาน,J&T Express,,n***_ton0,ภ******** ค*****,(+66)962****47,***** ,ไทย,สมุทรปราการ,บางพลี,,*********************************************,,PayLater,0.1,สเปรย์,1191785644778293022 ,,Unchecked,,,,,,,,,
|
||||
583380934433932947 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,35,0,35,,0,150,,04/04/2026 13:56:03 ,06/04/2026 17:51:55 ,04/04/2026 14:27:08 ,05/04/2026 12:43:16 ,06/04/2026 17:51:55 ," ",,,Fulfillment by seller,คลังหลัก,794510122291,การจัดส่งแบบมาตรฐาน,J&T Express,,u***6011261299130,ส**** ช*********,(+66)092*****01,***** ,ไทย,นนทบุรี,เมืองนนทบุรี,,*****************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191779141555816083 ,,Unchecked,,,,,,,,,
|
||||
583378131022284762 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333353499724148 ,P-SKU-OZO-MIXED-FRUITS,น้ำมันหอมระเหยอโรม่า ขนาดมาตรฐาน ใช้ได้นาน กลิ่น ผลไม้รวม กลิ่นไม่ฉุน,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,04/04/2026 10:49:37 ,06/04/2026 11:12:32 ,04/04/2026 11:16:15 ,05/04/2026 12:43:15 ,06/04/2026 11:12:32 ," ",,,Fulfillment by seller,คลังหลัก,794485386074,การจัดส่งแบบมาตรฐาน,J&T Express,,k***2545_3,K*************,(+66)953****22,***** ,ไทย,ปทุมธานี,ธัญบุรี,,***************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1191754599306659802 ,,Unchecked,,,,,,,,,
|
||||
583370731464918547 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333417262254452 ,P-SKU-OZO-ORANGE-JASMINE,อโรม่าหอมระเหย ขนาด 85ml กลิ่น ดอกแก้ว กลิ่นหอมสดชื่น,60 ml,1,0,150,150,0,0,150,0,35,0,35,,0,150,,03/04/2026 21:10:10 ,06/04/2026 13:29:05 ,04/04/2026 06:39:37 ,04/04/2026 08:49:42 ,06/04/2026 13:29:05 ," ",,,Fulfillment by seller,คลังหลัก,794441690711,การจัดส่งแบบมาตรฐาน,J&T Express,,a***sudarat1804,อ**,(+66)065*****13,***** ,ไทย,กรุงเทพฯ,พระโขนง,,****************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1191702757548328467 ,,Unchecked,,,,,,,,,
|
||||
583353502539875446 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,150,,02/04/2026 19:27:23 ,02/04/2026 19:27:25 ,03/04/2026 06:45:44 ,03/04/2026 09:23:01 ,05/04/2026 11:38:55 ," ",,,Fulfillment by seller,คลังหลัก,794449728100,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rudee,ส***** ,(+66)844****46,***** ,ไทย,นครศรีธรรมราช,นครศรีธรรมราช,,**************************************************,*********,PayLater,0.1,สเปรย์,1191585884146205814 ,,Unchecked,,,,,,,,,
|
||||
583343241650800148 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133968855861620 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น แคนตาลูป สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,02/04/2026 05:46:50 ,03/04/2026 20:01:38 ,02/04/2026 06:38:27 ,02/04/2026 08:39:56 ,03/04/2026 20:01:38 ," ",,,Fulfillment by seller,คลังหลัก,794389346402,การจัดส่งแบบมาตรฐาน,J&T Express,,d***ik_story,ต*** ก******,(+66)649****89,***** ,ไทย,กรุงเทพฯ,วัฒนา,,************,*************************************,การเก็บเงินปลายทาง,0.1,น้ำมันหอมระเหย,1191321816832509460 ,,Unchecked,,,,,,,,,
|
||||
583334596480829334 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,38,38,0,0,,0,188,,01/04/2026 15:18:21 ,01/04/2026 15:18:23 ,01/04/2026 16:41:19 ,02/04/2026 08:39:56 ,04/04/2026 20:02:14 ," ",,,Fulfillment by seller,คลังหลัก,794378253222,การจัดส่งแบบมาตรฐาน,J&T Express,,a***nt1993,แ*** แ** แ***,(+66)886****70,***** ,ไทย,อุดรธานี,บ้านดุง,,****************************************************,,TrueMoney,0.1,สเปรย์,1191447115912939414 ,,Unchecked,,,,,,,,,
|
||||
583327664430483328 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357395316 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอโซน,1,0,150,150,0,0,150,0,38,0,38,,0,150,,01/04/2026 05:23:39 ,02/04/2026 18:00:35 ,01/04/2026 06:42:29 ,01/04/2026 08:38:18 ,02/04/2026 18:00:35 ," ",,,Fulfillment by seller,คลังหลัก,794227758763,การจัดส่งแบบมาตรฐาน,J&T Express,,o***a.jaecoo_chonburi,O**** j***** ต*******,(+66)994****11,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,*************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191382601518778240 ,,Unchecked,,,,,,,,,
|
||||
583321700854891630 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,12,0,138,0,38,0,38,,0,276,,31/03/2026 18:39:17 ,06/04/2026 16:54:43 ,31/03/2026 19:56:32 ,01/04/2026 08:38:17 ,06/04/2026 16:54:43 ," ",,,Fulfillment by seller,คลังหลัก,794279898591,การจัดส่งแบบมาตรฐาน,J&T Express,,p***tira6395,ภ****** พ*****,(+66)061*****69,***** ,ไทย,ชลบุรี,บางละมุง,,**********,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191350928703718510 ,,Unchecked,,,,,,,,,
|
||||
583321700854891630 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,12,0,138,0,38,0,38,,0,276,,31/03/2026 18:39:17 ,06/04/2026 16:54:43 ,31/03/2026 19:56:32 ,01/04/2026 08:38:17 ,06/04/2026 16:54:43 ," ",,,Fulfillment by seller,คลังหลัก,794279898591,การจัดส่งแบบมาตรฐาน,J&T Express,,p***tira6395,ภ****** พ*****,(+66)061*****69,***** ,ไทย,ชลบุรี,บางละมุง,,**********,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191350928703718510 ,,Unchecked,,,,,,,,,
|
||||
583303742231446766 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,150,150,0,0,150,0,88,0,88,,0,150,,30/03/2026 15:21:51 ,02/04/2026 19:05:28 ,30/03/2026 17:54:26 ,31/03/2026 08:51:29 ,02/04/2026 19:05:28 ," ",,,Fulfillment by seller,คลังหลัก,794179265876,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1191211958398190830 ,,Unchecked,,,,,,,,,
|
||||
583296224432522482 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,1,150,150,0,0,150,0,35,0,35,,0,150,150,30/03/2026 02:13:39 ," "," "," "," ",30/03/2026 02:18:08 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,o***in2828,O*********,(+66)619****47,***** ,ไทย,กรุงเทพฯ,คันนายาว,,**************************************************************************************,******,การเก็บเงินปลายทาง,0.1,สเปรย์,1191136718090765554 ,,Unchecked,,,,,,,,,
|
||||
583272091767047548 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188686196 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,30 ml,1,0,79,79,6.32,0,72.68,0,38,0,38,,0,72.68,,28/03/2026 14:12:32 ,31/03/2026 12:38:28 ,28/03/2026 16:34:09 ,29/03/2026 09:20:13 ,31/03/2026 12:38:28 ," ",,,Fulfillment by seller,คลังหลัก,794053009691,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1675527008006,เ********* ล******,(+66)943****27,***** ,ไทย,อุบลราชธานี,เมืองอุบลราชธานี,,*****************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1190992912247915900 ,,Unchecked,,,,,,,,,
|
||||
583271825248191868 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732333329188686196 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,30 ml,1,1,79,79,1.1,0,77.9,0,38,0,38,,0,77.9,77.9,28/03/2026 13:52:25 ," "," "," "," ",28/03/2026 13:57:02 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,u***1675527008006,เ********* ล******,(+66)943****27,***** ,ไทย,อุบลราชธานี,เมืองอุบลราชธานี,,*****************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1190990036622148988 ,,Unchecked,,,,,,,,,
|
||||
583265405096986292 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,0,38,0,38,,0,150,,28/03/2026 01:35:57 ,29/03/2026 14:30:50 ,28/03/2026 06:37:08 ,28/03/2026 12:29:40 ,29/03/2026 14:30:50 ," ",,,Fulfillment by seller,คลังหลัก,794001380163,การจัดส่งแบบมาตรฐาน,J&T Express,,n***a29,น****** ห*****,(+66)944****24,***** ,ไทย,สระบุรี,หนองแค,,**********************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190933987121137332 ,,Unchecked,,,,,,,,,
|
||||
583261574744802438 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623645044 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,2,0,200,400,0,0,400,88,88,0,0,,0,488,,27/03/2026 20:07:54 ,27/03/2026 20:08:11 ,28/03/2026 06:37:51 ,28/03/2026 12:29:40 ,30/03/2026 12:52:46 ," ",,,Fulfillment by seller,คลังหลัก,794007382151,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1474995459245,อ*** ท******,(+66)818****77,***** ,ไทย,กระบี่,เมืองกระบี่,,*************************,,Mbanking,0.2,สเปรย์,1190916230978176134 ,,Unchecked,,,,,,,,,
|
||||
583260870310266041 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,10.5,0,139.5,0,35,0,35,,0,418.5,,27/03/2026 19:20:24 ,29/03/2026 11:56:22 ,27/03/2026 19:29:21 ,28/03/2026 12:29:41 ,29/03/2026 11:56:22 ," ",,,Fulfillment by seller,คลังหลัก,793946078015,การจัดส่งแบบมาตรฐาน,J&T Express,,c***chanamassage1,E*** S***** M******,(+66)937****07,***** ,泰國,Bangkok,Bang Rak,,***************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190912604990506169 ,,Unchecked,,,,,,,,,
|
||||
583260870310266041 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435534708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,150,150,10.5,0,139.5,0,35,0,35,,0,418.5,,27/03/2026 19:20:24 ,29/03/2026 11:56:22 ,27/03/2026 19:29:21 ,28/03/2026 12:29:41 ,29/03/2026 11:56:22 ," ",,,Fulfillment by seller,คลังหลัก,793946078015,การจัดส่งแบบมาตรฐาน,J&T Express,,c***chanamassage1,E*** S***** M******,(+66)937****07,***** ,泰國,Bangkok,Bang Rak,,***************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190912604990506169 ,,Unchecked,,,,,,,,,
|
||||
583260870310266041 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,10.5,0,139.5,0,35,0,35,,0,418.5,,27/03/2026 19:20:24 ,29/03/2026 11:56:22 ,27/03/2026 19:29:21 ,28/03/2026 12:29:41 ,29/03/2026 11:56:22 ," ",,,Fulfillment by seller,คลังหลัก,793946078015,การจัดส่งแบบมาตรฐาน,J&T Express,,c***chanamassage1,E*** S***** M******,(+66)937****07,***** ,泰國,Bangkok,Bang Rak,,***************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190912604990506169 ,,Unchecked,,,,,,,,,
|
||||
583248308340754250 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134499099117940 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกบัว สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,26/03/2026 22:10:14 ,26/03/2026 22:10:43 ,27/03/2026 06:59:21 ,27/03/2026 12:23:42 ,28/03/2026 17:15:57 ," ",,,Fulfillment by seller,คลังหลัก,793924122611,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ty9jk,ธ*** ส****** ห*** 6**,(+66)953****21,***** ,ไทย,กรุงเทพฯ,พระโขนง,,****************************************************************************************,,Mbanking,0.1,สเปรย์,1190812997195564874 ,,Unchecked,,,,,,,,,
|
||||
583247486368121880 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,3,38,0,35,0.15,0,152.85,,26/03/2026 21:15:57 ,26/03/2026 21:16:03 ,27/03/2026 06:57:30 ,27/03/2026 12:23:42 ,28/03/2026 12:18:46 ," ",,,Fulfillment by seller,คลังหลัก,793960522611,การจัดส่งแบบมาตรฐาน,J&T Express,,u***35161756327993,ธ**** จ******,(+66)098*****63,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,******************************************************************,,PayLater,0.1,สเปรย์,1190809135206663192 ,,Unchecked,,,,,,,,,
|
||||
583241047871947897 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256170956148 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น ดอกอัญชัน,1,0,79,79,0,0,79,38,38,0,0,,0,117,,26/03/2026 13:23:21 ,28/03/2026 15:04:39 ,26/03/2026 15:11:08 ,27/03/2026 12:23:42 ,28/03/2026 15:04:39 ," ",,,Fulfillment by seller,คลังหลัก,793987434435,การจัดส่งแบบมาตรฐาน,J&T Express,,t***9.2,ม****,(+66)966****65,***** ,ไทย,ระยอง,นิคมพัฒนา,,********************************************************************************************,,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1190763391578375289 ,,Unchecked,,,,,,,,,
|
||||
583235085333726238 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,14.62,0,135.38,0,35,0,35,,0,135.38,,26/03/2026 02:19:07 ,31/03/2026 23:28:57 ,26/03/2026 06:36:27 ,26/03/2026 09:01:37 ,31/03/2026 23:28:57 ," ",,,Fulfillment by seller,คลังหลัก,793800930016,การจัดส่งแบบมาตรฐาน,J&T Express,,b***76378,เ**,(+66)960****71,***** ,ไทย,กรุงเทพฯ,บางพลัด,,********************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190710775099458590 ,,Unchecked,,,,,,,,,
|
||||
583234735841445372 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134854286412148 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ทับทิม สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,38,38,0,0,0.18,0,217.82,,26/03/2026 00:58:22 ,26/03/2026 00:58:25 ,26/03/2026 06:32:05 ,26/03/2026 09:01:39 ,27/03/2026 11:02:48 ," ",,,Fulfillment by seller,คลังหลัก,793845418074,การจัดส่งแบบมาตรฐาน,J&T Express,,t***hanutporn246,t********* t**,(+66)970****49,***** ,ไทย,ชลบุรี,พานทอง,,*************************************************,************************************************************************,PayLater,0.1,น้ำมันหอมระเหย,1190708805001709052 ,,Unchecked,,,,,,,,,
|
||||
583222421494137945 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134462379362676 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น หมากฝรั่ง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,85 ml,1,0,150,150,0,0,150,0,35,0,35,,0,150,,25/03/2026 09:04:06 ,26/03/2026 19:35:59 ,25/03/2026 09:25:53 ,25/03/2026 12:37:16 ,26/03/2026 19:35:59 ," ",,,Fulfillment by seller,คลังหลัก,793857027692,การจัดส่งแบบมาตรฐาน,J&T Express,,s***dasupapisarn,ส***** ศ*******,(+66)084*****43,***** ,ไทย,กรุงเทพฯ,ประเวศ,,****************************************************************,*,การเก็บเงินปลายทาง,0.1,สเปรย์,1190618944216925273 ,,Unchecked,,,,,,,,,
|
||||
583217742887683905 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134462379231604 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น หมากฝรั่ง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,117,,24/03/2026 21:51:41 ,27/03/2026 14:09:08 ,25/03/2026 06:44:15 ,25/03/2026 08:34:25 ,27/03/2026 14:09:08 ," ",,,Fulfillment by seller,คลังหลัก,793807646435,การจัดส่งแบบมาตรฐาน,J&T Express,,t***_art,G*** T***,(+66)084*****69,***** ,ไทย,เชียงใหม่,สันกำแพง,,**************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190584677590665025 ,,Unchecked,,,,,,,,,
|
||||
583217691995768641 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732134462379231604 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น หมากฝรั่ง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,1,79,79,0,0,79,29,29,0,0,,0,108,79,24/03/2026 21:49:48 ," "," "," "," ",24/03/2026 21:51:15 ,User,จำเป็นต้องเปลี่ยนที่อยู่จัดส่ง,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,t***_art,อ**** ห*****,(+66)991****41,***** ,ไทย,ราชบุรี,บ้านโป่ง,,**************************************,*************************************************************************************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1190584535773644609 ,,Unchecked,,,,,,,,,
|
||||
583209825412875502 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,8,88,0,80,,0,158,,24/03/2026 11:46:44 ,26/03/2026 17:29:19 ,24/03/2026 11:50:40 ,24/03/2026 12:16:09 ,26/03/2026 17:29:19 ," ",,,Fulfillment by seller,คลังหลัก,793857271130,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190527477062665454 ,,Unchecked,,,,,,,,,
|
||||
583199326700144426 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188882804 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,185,,23/03/2026 16:53:08 ,26/03/2026 14:09:54 ,23/03/2026 18:22:12 ,24/03/2026 08:30:15 ,26/03/2026 14:09:54 ," ",,,Fulfillment by seller,คลังหลัก,793716981735,การจัดส่งแบบมาตรฐาน,J&T Express,,j***46029,ร**************************************,(+66)627****75,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,******************************************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1190451887457797930 ,,Unchecked,,,,,,,,,
|
||||
583194158356989558 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134728508803444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกการะเวก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,23/03/2026 09:38:18 ,24/03/2026 17:39:43 ,23/03/2026 10:07:56 ,23/03/2026 12:15:07 ,24/03/2026 17:39:43 ," ",,,Fulfillment by seller,คลังหลัก,793707449555,การจัดส่งแบบมาตรฐาน,J&T Express,,p***itta.63,พ***** ว*******,(+66)092*****61,***** ,ไทย,ปทุมธานี,คลองหลวง,,********************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190405953739589238 ,,Unchecked,,,,,,,,,
|
||||
583191027926598847 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,20,0,180,0,88,0,88,,0,180,,23/03/2026 00:24:44 ,25/03/2026 13:51:59 ,23/03/2026 06:31:23 ,23/03/2026 12:15:07 ,25/03/2026 13:51:59 ," ",,,Fulfillment by seller,คลังหลัก,793724759273,การจัดส่งแบบมาตรฐาน,J&T Express,,e***hsiribanchong,เ****,(+66)644****52,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*********************************************,**************,การเก็บเงินปลายทาง,0.1,สเปรย์,1190375468610258111 ,,Unchecked,,,,,,,,,
|
||||
583190662236112179 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134195930499444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,9,0,171,0,38,0,38,,0,171,,22/03/2026 23:39:14 ,24/03/2026 15:15:14 ,23/03/2026 06:30:28 ,23/03/2026 12:15:07 ,24/03/2026 15:15:14 ," ",,,Fulfillment by seller,คลังหลัก,793763551276,การจัดส่งแบบมาตรฐาน,J&T Express,,d***6wha2xxl,ช***** จ*********,(+66)086*****59,***** ,ไทย,ชลบุรี,บ้านบึง,,*******************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190373002036413747 ,,Unchecked,,,,,,,,,
|
||||
583186349994836993 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,35,0,35,,0,185,,22/03/2026 18:07:08 ,24/03/2026 12:40:46 ,22/03/2026 20:21:56 ,23/03/2026 12:15:07 ,24/03/2026 12:40:46 ," ",,,Fulfillment by seller,คลังหลัก,793716539173,การจัดส่งแบบมาตรฐาน,J&T Express,,n***nkrong.sp,ไ******************,(+66)988****55,***** ,ไทย,กรุงเทพฯ,ทุ่งครุ,,*****************************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1190353946635830273 ,,Unchecked,,,,,,,,,
|
||||
583183480766040006 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,35,0,35,,0,150,,22/03/2026 13:58:39 ,24/03/2026 15:25:28 ,22/03/2026 17:08:40 ,23/03/2026 12:15:07 ,24/03/2026 15:25:28 ," ",,,Fulfillment by seller,คลังหลัก,793600181080,การจัดส่งแบบมาตรฐาน,J&T Express,,a***met2565,น********** ธ************** ,(+66)086*****63,***** ,ราชอาณาจักรไทย,ปทุมธานี,ธัญบุรี,,*****************************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190335033710774214 ,,Unchecked,,,,,,,,,
|
||||
583176226423211448 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435534708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,150,150,0,0,150,38,38,0,0,,0,338,,21/03/2026 22:03:34 ,24/03/2026 13:16:54 ,22/03/2026 07:56:38 ,22/03/2026 09:19:41 ,24/03/2026 13:16:54 ," ",,,Fulfillment by seller,คลังหลัก,793626918825,การจัดส่งแบบมาตรฐาน,J&T Express,,p***8155,W****** t******,(+66)633****77,***** ,ไทย,เชียงใหม่,หางดง,,*************************************************,*****************,การเก็บเงินปลายทาง,0.1,สเปรย์,1190289927425263032 ,,Unchecked,,,,,,,,,
|
||||
583176226423211448 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,38,38,0,0,,0,338,,21/03/2026 22:03:34 ,24/03/2026 13:16:54 ,22/03/2026 07:56:38 ,22/03/2026 09:19:41 ,24/03/2026 13:16:54 ," ",,,Fulfillment by seller,คลังหลัก,793626918825,การจัดส่งแบบมาตรฐาน,J&T Express,,p***8155,W****** t******,(+66)633****77,***** ,ไทย,เชียงใหม่,หางดง,,*************************************************,*****************,การเก็บเงินปลายทาง,0.1,สเปรย์,1190289927425263032 ,,Unchecked,,,,,,,,,
|
||||
583161086033888494 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,8,88,0,80,,0,158,,20/03/2026 16:52:20 ,23/03/2026 16:46:25 ,20/03/2026 17:42:42 ,21/03/2026 09:13:14 ,23/03/2026 16:46:25 ," ",,,Fulfillment by seller,คลังหลัก,793640231156,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1190194624815006958 ,,Unchecked,,,,,,,,,
|
||||
583158210024080666 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215020771149172 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น ตะไคร้,2,0,79,158,0,0,158,38,38,0,0,,0,196,,20/03/2026 12:21:10 ,22/03/2026 15:32:16 ,20/03/2026 15:02:21 ,21/03/2026 09:13:14 ,22/03/2026 15:32:16 ," ",,,Fulfillment by seller,คลังหลัก,793519998036,การจัดส่งแบบมาตรฐาน,J&T Express,,c***akhaw1,ส****** แ********,(+66)627****78,***** ,ไทย,ขอนแก่น,เมืองขอนแก่น,,******************************************,,การเก็บเงินปลายทาง,0.24,น้ำมันหอมระเหย,1190169611871880474 ,,Unchecked,,,,,,,,,
|
||||
583128677533517566 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134517081213300 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะกรูด สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,38,38,0,0,,0,218,,18/03/2026 04:45:40 ,19/03/2026 20:39:41 ,18/03/2026 06:26:31 ,18/03/2026 09:32:12 ,19/03/2026 20:39:41 ," ",,,Fulfillment by seller,คลังหลัก,793458614616,การจัดส่งแบบมาตรฐาน,J&T Express,,u***3551729750803,ล******* ภ***,(+66)633****35,***** ,ไทย,ชลบุรี,บางละมุง,,****************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1189945697230096126 ,,Unchecked,,,,,,,,,
|
||||
583125865879275502 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134348768512372 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น แอปเปิ้ลแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,29,0,29,,0,180,,17/03/2026 21:46:33 ,19/03/2026 16:15:07 ,18/03/2026 06:25:38 ,18/03/2026 09:32:11 ,19/03/2026 16:15:07 ," ",,,Fulfillment by seller,คลังหลัก,793438528606,การจัดส่งแบบมาตรฐาน,J&T Express,,u***9056027634561,อ******** พ********,(+66)833****23,***** ,ไทย,กาญจนบุรี,เลาขวัญ,,****************************************,**********,การเก็บเงินปลายทาง,0.1,น้ำมันหอมระเหย,1189931298730772462 ,,Unchecked,,,,,,,,,
|
||||
583121729431897514 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,150,300,0,0,300,0,88,0,88,,0,300,,17/03/2026 16:03:52 ,20/03/2026 16:23:07 ,17/03/2026 19:28:19 ,18/03/2026 09:32:12 ,20/03/2026 16:23:07 ," ",,,Fulfillment by seller,คลังหลัก,793486316560,การจัดส่งแบบมาตรฐาน,J&T Express,,u***54560373848393,พ*****,(+66)967****21,***** ,ไทย,กระบี่,เมืองกระบี่,,***********************************************,********************************************************,การเก็บเงินปลายทาง,0.2,สเปรย์,1189905278447420842 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583058957464602056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,38,0,38,,0,1050,,13/03/2026 14:13:30 ,16/03/2026 19:59:19 ,13/03/2026 16:39:26 ,14/03/2026 09:04:45 ,16/03/2026 19:59:19 ," ",,,Fulfillment by seller,คลังหลัก,793264114263,การจัดส่งแบบมาตรฐาน,J&T Express,,u***86348756,ส****** ม*****,(+66)093*****38,***** ,ไทย,พังงา,ท้ายเหมือง,,*******************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189416110448674248 ,,Unchecked,,,,,,,,,
|
||||
583043381345158657 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,0,35,0,35,,0,150,,12/03/2026 15:11:56 ,14/03/2026 12:45:20 ,12/03/2026 19:22:01 ,13/03/2026 10:18:54 ,14/03/2026 12:45:20 ," ",,,Fulfillment by seller,คลังหลัก,793160182636,การจัดส่งแบบมาตรฐาน,J&T Express,,n***chanartcharoe0,น**,(+66)092*****88,***** ,ไทย,สมุทรปราการ,บางพลี,,******************************,****************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189293898660414977 ,,Unchecked,,,,,,,,,
|
||||
583032311422420892 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,15.8,0,63.2,15,35,0,20,,0,78.2,,11/03/2026 21:31:58 ,15/03/2026 19:58:14 ,12/03/2026 06:45:36 ,12/03/2026 11:44:02 ,15/03/2026 19:58:14 ," ",,,Fulfillment by seller,คลังหลัก,793154719221,การจัดส่งแบบมาตรฐาน,J&T Express,,u***4139750090618,ร******* ท******,(+66)954****32,***** ,ไทย,กรุงเทพฯ,บางคอแหลม,,**********************************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1189202061458573212 ,,Unchecked,,,,,,,,,
|
||||
583029453209962305 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,300,,11/03/2026 18:30:15 ,14/03/2026 12:59:49 ,11/03/2026 19:23:29 ,12/03/2026 11:44:02 ,14/03/2026 12:59:49 ," ",,,Fulfillment by seller,คลังหลัก,793161404151,การจัดส่งแบบมาตรฐาน,J&T Express,,y***aya.yoss.bodv,ย***,(+66)093*****91,***** ,ไทย,นครราชสีมา,สีคิ้ว,,*******************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1189185389782861633 ,,Unchecked,,,,,,,,,
|
||||
583029453209962305 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,300,,11/03/2026 18:30:15 ,14/03/2026 12:59:49 ,11/03/2026 19:23:29 ,12/03/2026 11:44:02 ,14/03/2026 12:59:49 ," ",,,Fulfillment by seller,คลังหลัก,793161404151,การจัดส่งแบบมาตรฐาน,J&T Express,,y***aya.yoss.bodv,ย***,(+66)093*****91,***** ,ไทย,นครราชสีมา,สีคิ้ว,,*******************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1189185389782861633 ,,Unchecked,,,,,,,,,
|
||||
583028182987342881 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,450,,11/03/2026 16:52:00 ,13/03/2026 15:40:34 ,11/03/2026 19:22:16 ,12/03/2026 11:44:02 ,13/03/2026 15:40:34 ," ",,,Fulfillment by seller,คลังหลัก,793147706153,การจัดส่งแบบมาตรฐาน,J&T Express,,c***uahua_phetchabun,ค** ร**,(+66)091*****14,***** ,ไทย,ชลบุรี,บางละมุง,,********************************************************,*************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189175186449531937 ,,Unchecked,,,,,,,,,
|
||||
583028182987342881 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357198708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไม้หอม,1,0,150,150,0,0,150,0,38,0,38,,0,450,,11/03/2026 16:52:00 ,13/03/2026 15:40:34 ,11/03/2026 19:22:16 ,12/03/2026 11:44:02 ,13/03/2026 15:40:34 ," ",,,Fulfillment by seller,คลังหลัก,793147706153,การจัดส่งแบบมาตรฐาน,J&T Express,,c***uahua_phetchabun,ค** ร**,(+66)091*****14,***** ,ไทย,ชลบุรี,บางละมุง,,********************************************************,*************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189175186449531937 ,,Unchecked,,,,,,,,,
|
||||
583028182987342881 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357788532 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกพุด,1,0,150,150,0,0,150,0,38,0,38,,0,450,,11/03/2026 16:52:00 ,13/03/2026 15:40:34 ,11/03/2026 19:22:16 ,12/03/2026 11:44:02 ,13/03/2026 15:40:34 ," ",,,Fulfillment by seller,คลังหลัก,793147706153,การจัดส่งแบบมาตรฐาน,J&T Express,,c***uahua_phetchabun,ค** ร**,(+66)091*****14,***** ,ไทย,ชลบุรี,บางละมุง,,********************************************************,*************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189175186449531937 ,,Unchecked,,,,,,,,,
|
||||
583026605374474007 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,11/03/2026 14:44:43 ,13/03/2026 13:37:55 ,11/03/2026 16:22:41 ,12/03/2026 11:44:02 ,13/03/2026 13:37:55 ," ",,,Fulfillment by seller,คลังหลัก,793078670053,การจัดส่งแบบมาตรฐาน,J&T Express,,t***eanjear,S T**********,(+66)654****78,***** ,ไทย,นครราชสีมา,เมืองนครราชสีมา,,******************************************************************,************************************************************************************************************************,การเก็บเงินปลายทาง,0.12,สเปรย์,1189158244619552535 ,,Unchecked,,,,,,,,,
|
||||
583009036922684585 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,38,0,38,0.18,0,184.82,,10/03/2026 13:51:49 ,10/03/2026 13:51:50 ,10/03/2026 15:52:09 ,11/03/2026 14:12:23 ,13/03/2026 11:24:56 ," ",,,Fulfillment by seller,คลังหลัก,793082474483,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ong90,ม********** ต*****,(+66)089*****50,***** ,ราชอาณาจักรไทย,กระบี่,คลองท่อม,,****************************************************************,,PayLater,0.12,สเปรย์,1189019672753243305 ,,Unchecked,,,,,,,,,
|
||||
583008314085573885 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133970455988596 ,,น้ำมันหอมระเหย Hom Aroma กลิ่น ดอกอัญชัน สำหรับเครื่องพ่นอโรม่า ช่วยเพิ่มความหอมในห้อง 30 ml,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,10/03/2026 12:55:14 ,13/03/2026 15:20:20 ,10/03/2026 15:50:12 ,11/03/2026 14:12:23 ,13/03/2026 15:20:20 ," ",,,Fulfillment by seller,คลังหลัก,793061371491,การจัดส่งแบบมาตรฐาน,J&T Express,,n***ttaranaparatt,น****** ณ******* ร********,(+66)093*****47,***** ,ไทย,กรุงเทพฯ,คลองสาน,,***********************************************,********************,การเก็บเงินปลายทาง,0.1,สเปรย์,1189012102368560381 ,,Unchecked,,,,,,,,,
|
||||
583003794338186827 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133911075785076 ,,น้ำมันหอมระเหย Hom Aroma กลิ่น ดอกอัญชัน สำหรับเครื่องพ่นอโรม่า ช่วยเพิ่มความหอมในห้อง 30 ml,60 ml,1,0,130,130,0,0,130,0,35,0,35,,0,130,,10/03/2026 07:19:23 ,11/03/2026 17:40:35 ,10/03/2026 09:55:34 ,10/03/2026 12:46:48 ,11/03/2026 17:40:35 ," ",,,Fulfillment by seller,คลังหลัก,793036080222,การจัดส่งแบบมาตรฐาน,J&T Express,,l***ed4,l******,(+66)062*****59,***** ,ไทย,กรุงเทพฯ,ตลิ่งชัน,,******************************,*****,การเก็บเงินปลายทาง,0.1,สเปรย์,1188967023408350795 ,,Unchecked,,,,,,,,,
|
||||
582971324427175150 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435927924 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แคนตาลูป,1,0,150,150,0,0,150,0,88,0,88,,0,300,,08/03/2026 10:49:01 ,11/03/2026 17:43:23 ,08/03/2026 17:41:16 ,09/03/2026 12:29:14 ,11/03/2026 17:43:23 ," ",,,Fulfillment by seller,คลังหลัก,792971951236,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1188724042772284654 ,,Unchecked,,,,,,,,,
|
||||
582971324427175150 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436058996 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แอปเปิ้ลเขียว,1,0,150,150,0,0,150,0,88,0,88,,0,300,,08/03/2026 10:49:01 ,11/03/2026 17:43:23 ,08/03/2026 17:41:16 ,09/03/2026 12:29:14 ,11/03/2026 17:43:23 ," ",,,Fulfillment by seller,คลังหลัก,792971951236,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.anne606,บ**** อ*****,(+66)095*****77,***** ,ไทย,ภูเก็ต,ถลาง,,*********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1188724042772284654 ,,Unchecked,,,,,,,,,
|
||||
582942367818155887 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484625348980 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,200,200,1.1,0,198.9,0,35,0,35,,0,198.9,,06/03/2026 18:01:35 ,08/03/2026 10:57:04 ,06/03/2026 19:30:52 ,07/03/2026 12:28:40 ,08/03/2026 10:57:04 ," ",,,Fulfillment by seller,คลังหลัก,792849811305,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ly_2354,ส*** อ********,(+66)091*****90,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตจอมทอง,,*****************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1188522312315406191 ,,Unchecked,,,,,,,,,
|
||||
582939883463410728 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,35,0,35,,0,450,,06/03/2026 14:55:09 ,08/03/2026 15:31:48 ,06/03/2026 15:37:13 ,06/03/2026 16:50:20 ,08/03/2026 15:31:48 ," ",,,Fulfillment by seller,คลังหลัก,792855350254,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7198168347675,ก***** ก********,(+66)061*****53,***** ,ไทย,กรุงเทพฯ,ประเวศ,,**************************,************,การเก็บเงินปลายทาง,0.1,สเปรย์,1188500324808754216 ,,Unchecked,,,,,,,,,
|
||||
582939883463410728 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,0,0,150,0,35,0,35,,0,450,,06/03/2026 14:55:09 ,08/03/2026 15:31:48 ,06/03/2026 15:37:13 ,06/03/2026 16:50:20 ,08/03/2026 15:31:48 ," ",,,Fulfillment by seller,คลังหลัก,792855350254,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7198168347675,ก***** ก********,(+66)061*****53,***** ,ไทย,กรุงเทพฯ,ประเวศ,,**************************,************,การเก็บเงินปลายทาง,0.1,สเปรย์,1188500324808754216 ,,Unchecked,,,,,,,,,
|
||||
582939883463410728 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435993460 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แอปเปิ้ลแดง,1,0,150,150,0,0,150,0,35,0,35,,0,450,,06/03/2026 14:55:09 ,08/03/2026 15:31:48 ,06/03/2026 15:37:13 ,06/03/2026 16:50:20 ,08/03/2026 15:31:48 ," ",,,Fulfillment by seller,คลังหลัก,792855350254,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7198168347675,ก***** ก********,(+66)061*****53,***** ,ไทย,กรุงเทพฯ,ประเวศ,,**************************,************,การเก็บเงินปลายทาง,0.1,สเปรย์,1188500324808754216 ,,Unchecked,,,,,,,,,
|
||||
582930820376790634 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435534708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,150,150,0,0,150,38,38,0,0,,0,188,,06/03/2026 00:55:36 ,07/03/2026 15:27:57 ,06/03/2026 06:57:16 ,06/03/2026 12:52:58 ,07/03/2026 15:27:57 ," ",,,Fulfillment by seller,คลังหลัก,792726556882,การจัดส่งแบบมาตรฐาน,J&T Express,,a***ayapronparanat,อ******** ผ******,(+66)865****96,***** ,ไทย,ชลบุรี,บางละมุง,,****************************************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1188420963787245162 ,,Unchecked,,,,,,,,,
|
||||
582906221594183551 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133970455988596 ,,น้ำมันหอมระเหย Hom Aroma กลิ่น ดอกอัญชัน สำหรับเครื่องพ่นอโรม่า ช่วยเพิ่มความหอมในห้อง 30 ml,120 ml,2,0,180,360,0,0,360,0,88,0,88,,0,360,,04/03/2026 16:25:33 ,07/03/2026 21:10:55 ,04/03/2026 19:52:24 ,05/03/2026 13:26:39 ,07/03/2026 21:10:55 ," ",,,Fulfillment by seller,คลังหลัก,792758265154,การจัดส่งแบบมาตรฐาน,J&T Express,,k***51717,k***,(+66)832****11,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,******************************************,,การเก็บเงินปลายทาง,0.2,สเปรย์,1188238876640053119 ,,Unchecked,,,,,,,,,
|
||||
582892094021403719 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,1.27,0,148.73,0,35,0,35,,0,148.73,,03/03/2026 20:44:19 ,06/03/2026 16:22:42 ,04/03/2026 07:02:14 ,04/03/2026 12:40:57 ,06/03/2026 16:22:42 ," ",,,Fulfillment by seller,คลังหลัก,792683242643,การจัดส่งแบบมาตรฐาน,J&T Express,,m***ai19,พ*******,(+66)095*****79,***** ,ไทย,กรุงเทพมหานคร,เขตสวนหลวง,,*********************************************************************************************,***************,การเก็บเงินปลายทาง,0.1,สเปรย์,1188126106640680007 ,,Unchecked,,,,,,,,,
|
||||
582890058722412017 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,29,0,29,,0,150,,03/03/2026 18:57:09 ,05/03/2026 15:40:49 ,03/03/2026 19:01:24 ,04/03/2026 12:40:57 ,05/03/2026 15:40:49 ," ",,,Fulfillment by seller,คลังหลัก,792628087451,การจัดส่งแบบมาตรฐาน,J&T Express,,w***ya27,w*******,(+66)631****99,***** ,ไทย,ประจวบคีรีขันธ์,หัวหิน,,***************************************************************************,*****,การเก็บเงินปลายทาง,0.1,สเปรย์,1188113608243119601 ,,Unchecked,,,,,,,,,
|
||||
582882053977703836 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333356117231988 ,P-SKU-OZO-GREEN-APPLE,aroma oil กลิ่น แอปเปิ้ลเขียว ขนาดมาตรฐาน ใช้ได้นาน เหมาะสำหรับคนแพ้ง่าย,60 ml,1,0,150,150,0,0,150,0,38,0,38,,0,150,,03/03/2026 10:45:42 ,04/03/2026 11:20:03 ,03/03/2026 11:35:40 ,03/03/2026 13:04:09 ,04/03/2026 11:20:03 ," ",,,Fulfillment by seller,คลังหลัก,792639162265,การจัดส่งแบบมาตรฐาน,J&T Express,,m***8y,อ******** บ*****,(+66)945****85,***** ,ไทย,ชลบุรี,ศรีราชา,,***********************************************,********************************************************,การเก็บเงินปลายทาง,0.12,สเปรย์,1188045281043449244 ,,Unchecked,,,,,,,,,
|
||||
582854414646412315 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357722996 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกพิกุล,1,0,150,150,0,0,150,0,35,0,35,,0,150,,01/03/2026 21:20:32 ,03/03/2026 16:27:12 ,02/03/2026 06:00:31 ,02/03/2026 09:54:51 ,03/03/2026 16:27:12 ," ",,,Fulfillment by seller,คลังหลัก,792563729215,การจัดส่งแบบมาตรฐาน,J&T Express,,y***ntnaing891,K* P** G**,(+66)099*****23,***** ,ထိုင်း,Nonthaburi,Bang Kruai,,***************************************************************************************************************,***********,การเก็บเงินปลายทาง,0.1,สเปรย์,1187828417356268571 ,,Unchecked,,,,,,,,,
|
||||
582854256829826075 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,35,0,35,,0,150,,01/03/2026 21:15:16 ,03/03/2026 16:27:12 ,02/03/2026 05:57:57 ,02/03/2026 09:54:51 ,03/03/2026 16:27:12 ," ",,,Fulfillment by seller,คลังหลัก,792566021251,การจัดส่งแบบมาตรฐาน,J&T Express,,y***ntnaing891,K* P** G**,(+66)099*****23,***** ,ထိုင်း,Nonthaburi,Bang Kruai,,***************************************************************************************************************,***********,การเก็บเงินปลายทาง,0.1,สเปรย์,1187828338012226587 ,,Unchecked,,,,,,,,,
|
||||
582850722443134345 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,88,88,0,0,,0,238,,01/03/2026 17:53:59 ,01/03/2026 17:56:07 ,01/03/2026 19:14:49 ,02/03/2026 09:54:51 ,04/03/2026 19:39:41 ," ",,,Fulfillment by seller,คลังหลัก,792491476070,การจัดส่งแบบมาตรฐาน,J&T Express,,y***.hanwong,เ******* ห******,(+66)986****90,***** ,ไทย,สุราษฎร์ธานี,เกาะพะงัน,,*************************************,**********,TrueMoney,0.1,สเปรย์,1187809430476588425 ,,Unchecked,,,,,,,,,
|
||||
582825457071916569 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,0,88,0,88,,0,200,,28/02/2026 11:39:28 ,03/03/2026 13:07:37 ,28/02/2026 12:47:14 ,28/02/2026 13:00:25 ,03/03/2026 13:07:37 ," ",,,Fulfillment by seller,คลังหลัก,792436238484,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ii554409,พ******** ส************** B** c***,(+66)963****08,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*******,*****,การเก็บเงินปลายทาง,0.1,สเปรย์,1187634099795691033 ,,Unchecked,,,,,,,,,
|
||||
582762703194064366 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,0,38,0,38,,0,150,,24/02/2026 21:08:14 ,27/02/2026 16:17:09 ,25/02/2026 08:54:38 ,25/02/2026 12:21:42 ,27/02/2026 16:17:09 ," ",,,Fulfillment by seller,คลังหลัก,792208091441,การจัดส่งแบบมาตรฐาน,J&T Express,,d***2135,ค******* ร******,(+66)092*****99,***** ,ไทย,นครศรีธรรมราช,ทุ่งสง,,****************************************************************************,***********************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1187164389024105966 ,,Unchecked,,,,,,,,,
|
||||
582750015229625838 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,150,,24/02/2026 05:30:32 ,26/02/2026 19:13:26 ,24/02/2026 07:00:54 ,24/02/2026 08:42:08 ,26/02/2026 19:13:26 ," ",,,Fulfillment by seller,คลังหลัก,792112185723,การจัดส่งแบบมาตรฐาน,J&T Express,,d***2135,ค******* ร******,(+66)092*****99,***** ,ไทย,สงขลา,หาดใหญ่,,**********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1187058561513194990 ,,Unchecked,,,,,,,,,
|
||||
582735967861704030 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406473357067636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ผลไม้รวม,1,1,150,150,0,0,150,0,27,0,27,,0,150,150,23/02/2026 09:57:33 ," ",23/02/2026 10:04:16 ,23/02/2026 13:28:10 ," ",03/03/2026 18:48:49 ,System,การจัดส่งพัสดุไม่สำเร็จ,Fulfillment by seller,คลังหลัก,792174238446,การจัดส่งแบบมาตรฐาน,J&T Express,,u***711401148,T** T****,(+66)903****85,***** ,ไทย,ชลบุรี,สัตหีบ,,**********************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186954771358909790 ,,Unchecked,,,,,,,,,
|
||||
582712270632617681 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188882804 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,120 ml,1,0,185,185,14.8,0,170.2,0,38,0,38,,0,170.2,,21/02/2026 21:55:55 ,24/02/2026 15:49:42 ,22/02/2026 18:35:30 ,23/02/2026 13:28:10 ,24/02/2026 15:49:42 ," ",,,Fulfillment by seller,คลังหลัก,792053779974,การจัดส่งแบบมาตรฐาน,J&T Express,,u***93239661022430,เ***********,(+66)979****99,***** ,ไทย,ชลบุรี,พนัสนิคม,,*****************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1186796553224947409 ,,Unchecked,,,,,,,,,
|
||||
582710596384753473 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,150,,21/02/2026 20:13:52 ,26/02/2026 13:52:46 ,22/02/2026 18:37:49 ,23/02/2026 13:28:09 ,26/02/2026 13:52:46 ," ",,,Fulfillment by seller,คลังหลัก,792039978973,การจัดส่งแบบมาตรฐาน,J&T Express,,y***aya.yoss.bodv,ย***,(+66)093*****91,***** ,ไทย,นครราชสีมา,สีคิ้ว,,*******************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186788522221471553 ,,Unchecked,,,,,,,,,
|
||||
582709848661067005 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134195930499444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,21/02/2026 19:21:34 ,24/02/2026 17:13:18 ,22/02/2026 18:32:01 ,23/02/2026 13:28:10 ,24/02/2026 17:13:18 ," ",,,Fulfillment by seller,คลังหลัก,792027373966,การจัดส่งแบบมาตรฐาน,J&T Express,,m***hai.pohkaew,ม****** โ*******,(+66)992****36,***** ,ไทย,ปทุมธานี,คลองหลวง,,**************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186784396591203581 ,,Unchecked,,,,,,,,,
|
||||
582700218755483113 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332589742452 ,P-SKU-OZO-GRAPE-SCENT,น้ำมันหอมระเหย ขนาด 120ml กลิ่น องุ่น อโรม่าแท้ 100%,120 ml,1,0,185,185,0.5,0,184.5,0,38,0,38,,0,184.5,,21/02/2026 08:20:16 ,22/02/2026 16:20:27 ,21/02/2026 08:25:49 ,21/02/2026 12:56:38 ,22/02/2026 16:20:27 ," ",,,Fulfillment by seller,คลังหลัก,792016619225,การจัดส่งแบบมาตรฐาน,J&T Express,,u***6005675707207,น** น**** ธ********,(+66)082*****93,***** ,ไทย,จังหวัดชลบุรี,บ้านบึง,,********************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1186711145630762473 ,,Unchecked,,,,,,,,,
|
||||
582689093987894948 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,0,0,150,0,38,0,38,,0,150,,20/02/2026 14:13:15 ,20/02/2026 14:14:00 ,20/02/2026 14:44:57 ,21/02/2026 12:56:40 ,22/02/2026 12:43:32 ," ",,,Fulfillment by seller,คลังหลัก,791927744915,การจัดส่งแบบมาตรฐาน,J&T Express,,d***p4zlm2dz,จ**,(+66)082*****72,***** ,ไทย,สระบุรี,เมืองสระบุรี,,***********************************,,PromptPay,0.1,สเปรย์,1186641337926583972 ,,Unchecked,,,,,,,,,
|
||||
582684355028288765 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,35,0,35,,0,150,,20/02/2026 09:06:20 ,21/02/2026 15:35:03 ,20/02/2026 09:15:34 ,20/02/2026 12:33:41 ,21/02/2026 15:35:03 ," ",,,Fulfillment by seller,คลังหลัก,791915558711,การจัดส่งแบบมาตรฐาน,J&T Express,,m***hai.pohkaew,ม****** โ*******,(+66)992****36,***** ,ไทย,ปทุมธานี,คลองหลวง,,**************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186604931527443709 ,,Unchecked,,,,,,,,,
|
||||
582674535831799527 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333333436664180 ,P-SKU-OZO-JASMINE,aroma oil ขนาด 85ml กลิ่น มะลิ ใช้กับเครื่องพ่นอโรม่า,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,19/02/2026 16:28:02 ,21/02/2026 15:02:44 ,19/02/2026 17:30:56 ,20/02/2026 12:33:42 ,21/02/2026 15:02:44 ," ",,,Fulfillment by seller,คลังหลัก,791930167442,การจัดส่งแบบมาตรฐาน,J&T Express,,m***56381,M M*,(+66)062*****26,***** ,Thailand,Bangkok,Bang Kho Laem,,************************************************,************************************,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1186545545657878247 ,,Unchecked,,,,,,,,,
|
||||
582672476151448905 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435534708 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,150,150,0,0,150,0,23,0,23,,0,150,,19/02/2026 13:57:04 ,19/02/2026 13:57:18 ,19/02/2026 15:45:14 ,20/02/2026 12:33:42 ,21/02/2026 13:21:45 ," ",,,Fulfillment by seller,คลังหลัก,791903130480,การจัดส่งแบบมาตรฐาน,J&T Express,,u***9068738461388,ฐ**** บ*******,(+66)631****89,***** ,ไทย,พระนครศรีอยุธยา,พระนครศรีอยุธยา,,********************,**********************,Mbanking,0.1,สเปรย์,1186529211346421065 ,,Unchecked,,,,,,,,,
|
||||
582657579393779664 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,21,0,21,,0,450,,18/02/2026 15:20:47 ,20/02/2026 10:52:32 ,18/02/2026 15:43:32 ,19/02/2026 12:11:36 ,20/02/2026 10:52:32 ," ",,,Fulfillment by seller,คลังหลัก,791827208933,การจัดส่งแบบมาตรฐาน,J&T Express,,p***dakae,ป**** อ********,(+66)626****52,***** ,ไทย,นนทบุรี,เมืองนนทบุรี,,**********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186430598101632976 ,,Unchecked,,,,,,,,,
|
||||
582657579393779664 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,0,21,0,21,,0,450,,18/02/2026 15:20:47 ,20/02/2026 10:52:32 ,18/02/2026 15:43:32 ,19/02/2026 12:11:36 ,20/02/2026 10:52:32 ," ",,,Fulfillment by seller,คลังหลัก,791827208933,การจัดส่งแบบมาตรฐาน,J&T Express,,p***dakae,ป**** อ********,(+66)626****52,***** ,ไทย,นนทบุรี,เมืองนนทบุรี,,**********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186430598101632976 ,,Unchecked,,,,,,,,,
|
||||
582657579393779664 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,0,21,0,21,,0,450,,18/02/2026 15:20:47 ,20/02/2026 10:52:32 ,18/02/2026 15:43:32 ,19/02/2026 12:11:36 ,20/02/2026 10:52:32 ," ",,,Fulfillment by seller,คลังหลัก,791827208933,การจัดส่งแบบมาตรฐาน,J&T Express,,p***dakae,ป**** อ********,(+66)626****52,***** ,ไทย,นนทบุรี,เมืองนนทบุรี,,**********************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1186430598101632976 ,,Unchecked,,,,,,,,,
|
||||
582639614906565665 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,2,0,150,300,18.1,0,281.9,0,35,0,35,,0,281.9,,17/02/2026 11:41:24 ,17/02/2026 11:41:46 ,17/02/2026 17:43:54 ,18/02/2026 08:49:07 ,19/02/2026 17:23:01 ," ",,,Fulfillment by seller,คลังหลัก,791854678381,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ayarsamuth,ส*** ช***,(+66)985****99,***** ,ไทย,นนทบุรี,บางกรวย,,****************************************************,,Mbanking,0.2,สเปรย์,1186309538901623841 ,,Unchecked,,,,,,,,,
|
||||
582631891228722567 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188686196 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,16/02/2026 20:19:45 ,20/02/2026 14:28:12 ,17/02/2026 05:56:21 ,18/02/2026 15:05:05 ,20/02/2026 14:28:12 ," ",,,Fulfillment by seller,คลังหลัก,791737548954,การจัดส่งแบบมาตรฐาน,J&T Express,,u***61275962816297,บ****** ค*******,(+66)946****95,***** ,ไทย,ลำพูน,อำเภอเมืองลำพูน,,************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1186262842915456391 ,,Unchecked,,,,,,,,,
|
||||
582619409541989398 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134195930499444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,2.02,0,177.98,0,35,0,35,,0,177.98,,15/02/2026 22:06:34 ,15/02/2026 22:06:55 ,16/02/2026 06:47:20 ,16/02/2026 11:30:48 ,17/02/2026 12:29:28 ," ",,,Fulfillment by seller,คลังหลัก,791713776361,การจัดส่งแบบมาตรฐาน,J&T Express,,m***anoy337,ส*** ท*****,(+66)991****09,***** ,ไทย,ปทุมธานี,ลำลูกกา,,****************************************************,,Mbanking,0.1,สเปรย์,1186168630191948822 ,,Unchecked,,,,,,,,,
|
||||
582607282330109756 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134568192280948 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กล้วย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,15/02/2026 03:53:33 ,15/02/2026 03:53:52 ,15/02/2026 05:59:48 ,16/02/2026 11:30:48 ,17/02/2026 15:03:39 ," ",,,Fulfillment by seller,คลังหลัก,791619724943,การจัดส่งแบบมาตรฐาน,J&T Express,,t***hakonbb,บ***,(+66)633****22,***** ,ไทย,สมุทรสาคร,กระทุ่มแบน,,*****************************************************,,Mbanking,0.1,น้ำมันหอมระเหย,1186097387832969020 ,,Unchecked,,,,,,,,,
|
||||
582564738035517437 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249250164 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,0,130,130,0,0,130,0,35,0,35,,0,130,,11/02/2026 23:25:04 ,11/02/2026 23:25:05 ,12/02/2026 06:06:28 ,12/02/2026 12:30:37 ,13/02/2026 17:37:43 ," ",,,Fulfillment by seller,คลังหลัก,791568913306,การจัดส่งแบบมาตรฐาน,J&T Express,,a***nt.78,A***** T********,(+66)645****54,***** ,ไทย,กรุงเทพฯ,จตุจักร,,*********************************************************************************,,TikTok Shop Balance,0.1,สเปรย์,1185790282430121981 ,,Unchecked,,,,,,,,,
|
||||
582546180089087165 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732134195930302836 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,1,79,79,0,0,79,0,38,0,38,,0,79,79,10/02/2026 18:53:39 ," "," "," "," ",10/02/2026 19:05:06 ,User,มีราคาที่ดีกว่า,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,j***yanooying,จ**** อ******,(+66)092*****26,***** ,ไทย,ชลบุรี,ศรีราชา,,**********************************,***************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1185657055519081661 ,,Unchecked,,,,,,,,,
|
||||
582542193636771087 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256169710964 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น ลาเวนเดอร์,8,0,79,632,0,0,632,0,35,0,35,,0,632,,10/02/2026 13:43:24 ,12/02/2026 15:25:25 ,10/02/2026 15:50:40 ,11/02/2026 09:59:40 ,12/02/2026 15:25:25 ," ",,,Fulfillment by seller,คลังหลัก,791496474521,การจัดส่งแบบมาตรฐาน,J&T Express,,l***kim265,L*** K*****,(+66)987****95,***** ,ไทย,กรุงเทพฯ,คลองเตย,,********************,*******************************************************************,การเก็บเงินปลายทาง,0.96,น้ำมันหอมระเหย,1185623257728320783 ,,Unchecked,,,,,,,,,
|
||||
582541028267034274 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134517081213300 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะกรูด สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,2,0,180,360,0,0,360,0,35,0,35,,0,360,,10/02/2026 12:12:19 ,10/02/2026 12:12:22 ,10/02/2026 12:20:15 ,10/02/2026 12:40:37 ,11/02/2026 11:37:59 ," ",,,Fulfillment by seller,คลังหลัก,791424268456,การจัดส่งแบบมาตรฐาน,J&T Express,,b***thivaphongrasri,บ****** ผ********,(+66)086*****71,***** ,ไทย,กรุงเทพมหานคร,เขตทุ่งครุ,,**********************************************************************************************,,บัตรเครดิต/เดบิต,0.2,สเปรย์,1185613473640908450 ,,Unchecked,,,,,,,,,
|
||||
582538924315411474 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134260190185844 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ซากุระ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,10/02/2026 09:34:47 ,10/02/2026 09:35:14 ,10/02/2026 10:08:18 ,10/02/2026 12:40:37 ,11/02/2026 16:06:51 ," ",,,Fulfillment by seller,คลังหลัก,791495764382,การจัดส่งแบบมาตรฐาน,J&T Express,,i***h169,P****** K*****,(+66)083*****54,***** ,ไทย,สมุทรสาคร,เมืองสมุทรสาคร,,********************************************************,,Mbanking,0.1,สเปรย์,1185594381411779602 ,,Unchecked,,,,,,,,,
|
||||
582535135569086212 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134761131903348 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกพิกุล สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0.05,0,78.95,0,38,0,38,,0,78.95,,09/02/2026 23:41:25 ,09/02/2026 23:41:48 ,10/02/2026 07:04:49 ,10/02/2026 12:40:37 ,11/02/2026 15:05:45 ," ",,,Fulfillment by seller,คลังหลัก,791436265163,การจัดส่งแบบมาตรฐาน,J&T Express,,a***ache0,ค****** ส*******,(+66)082*****01,***** ,ไทย,สระบุรี,หนองแค,,**************************************************,,Mbanking,0.1,สเปรย์,1185562236605662980 ,,Unchecked,,,,,,,,,
|
||||
582495907415099191 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,0,38,0,38,,0,300,,07/02/2026 15:15:20 ,10/02/2026 18:14:14 ,08/02/2026 19:01:27 ,09/02/2026 12:32:09 ,10/02/2026 18:14:14 ," ",,,Fulfillment by seller,คลังหลัก,791338962363,การจัดส่งแบบมาตรฐาน,J&T Express,,u***33681621631097,ภ*** ม******,(+66)063*****62,***** ,ไทย,นครปฐม,เมืองนครปฐม,,******************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1185285044467959607 ,,Unchecked,,,,,,,,,
|
||||
582495907415099191 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,0,38,0,38,,0,300,,07/02/2026 15:15:20 ,10/02/2026 18:14:14 ,08/02/2026 19:01:27 ,09/02/2026 12:32:09 ,10/02/2026 18:14:14 ," ",,,Fulfillment by seller,คลังหลัก,791338962363,การจัดส่งแบบมาตรฐาน,J&T Express,,u***33681621631097,ภ*** ม******,(+66)063*****62,***** ,ไทย,นครปฐม,เมืองนครปฐม,,******************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1185285044467959607 ,,Unchecked,,,,,,,,,
|
||||
582462897547216683 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134780451785076 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกพุด สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,35,35,0,0,0.08,0,113.92,,05/02/2026 14:30:41 ,05/02/2026 14:30:48 ,05/02/2026 15:45:15 ,06/02/2026 12:18:59 ,07/02/2026 16:25:02 ," ",,,Fulfillment by seller,คลังหลัก,791153487784,การจัดส่งแบบมาตรฐาน,J&T Express,,p***pimol.oh,พ***** ส*******,(+66)094*****16,***** ,ไทย,นนทบุรี,บางบัวทอง,,**********************,*******************,PayLater,0.1,สเปรย์,1185034428030945067 ,,Unchecked,,,,,,,,,
|
||||
582460304108192946 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,18,18,0,0,,0,168,,05/02/2026 11:32:06 ,08/02/2026 19:51:06 ,05/02/2026 11:42:45 ,05/02/2026 12:20:33 ,08/02/2026 19:51:06 ," ",,,Fulfillment by seller,คลังหลัก,791184323624,การจัดส่งแบบมาตรฐาน,J&T Express,,j***gatecherishes,จ****,(+66)061*****75,***** ,ไทย,ชุมพร,หลังสวน,,**********************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1185011771077592242 ,,Unchecked,,,,,,,,,
|
||||
582435831463052549 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134087248808308 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ยูคาลิปตัส สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,117,,03/02/2026 20:52:29 ,06/02/2026 15:25:27 ,04/02/2026 06:12:28 ,04/02/2026 12:21:25 ,06/02/2026 15:25:27 ," ",,,Fulfillment by seller,คลังหลัก,791085944736,การจัดส่งแบบมาตรฐาน,J&T Express,,t***_titima,ธ***** น****,(+66)643****36,***** ,ไทย,เชียงใหม่,เชียงดาว,,**********************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1184824376785863941 ,,Unchecked,,,,,,,,,
|
||||
582392112824157942 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623645044 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,200,200,0,0,200,0,28,0,28,,0,200,,01/02/2026 11:49:56 ,02/02/2026 15:41:01 ,01/02/2026 12:18:28 ,01/02/2026 15:07:05 ,02/02/2026 15:41:01 ," ",,,Fulfillment by seller,คลังหลัก,790890248046,การจัดส่งแบบมาตรฐาน,J&T Express,,e***h.o.y,ช***,(+66)095*****68,***** ,ไทย,กรุงเทพฯ,พระโขนง,,******************************,****************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1184496158547609334 ,,Unchecked,,,,,,,,,
|
||||
582378023861454020 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134195930499444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,31/01/2026 14:35:57 ,31/01/2026 14:36:26 ,01/02/2026 10:05:55 ,01/02/2026 15:07:52 ,03/02/2026 14:00:55 ," ",,,Fulfillment by seller,คลังหลัก,790853472964,การจัดส่งแบบมาตรฐาน,J&T Express,,d***irawangkaew,ด*** ร********,(+66)086*****95,***** ,ไทย,ปราจีนบุรี,ศรีมหาโพธิ,,***********,************************************,Mbanking,0.1,สเปรย์,1184407167809717444 ,,Unchecked,,,,,,,,,
|
||||
582363838089430185 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435927924 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แคนตาลูป,1,0,150,150,0,0,150,35,35,0,0,,0,185,,30/01/2026 17:18:13 ,01/02/2026 15:03:59 ,30/01/2026 17:33:30 ,31/01/2026 08:40:02 ,01/02/2026 15:03:59 ," ",,,Fulfillment by seller,คลังหลัก,790702693023,การจัดส่งแบบมาตรฐาน,J&T Express,,p***praphakaewmor,เ******* แ*******,(+66)089*****60,***** ,ราชอาณาจักรไทย,ปทุมธานี,คลองหลวง,,**************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1184307012942070953 ,,Unchecked,,,,,,,,,
|
||||
582326873471485759 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124609396 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,120 ml,1,0,185,185,0,0,185,0,88,0,88,,0,185,,28/01/2026 10:47:49 ,28/01/2026 10:47:50 ,28/01/2026 10:58:48 ,28/01/2026 12:33:54 ,30/01/2026 15:01:03 ," ",,,Fulfillment by seller,คลังหลัก,790696929724,การจัดส่งแบบมาตรฐาน,J&T Express,,s***vimolautcha,p********,(+66)806****25,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*********************************************,****************************,PayLater,0.12,สเปรย์,1184023358708614975 ,,Unchecked,,,,,,,,,
|
||||
582321042879776522 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,3,0,150,450,0,0,450,0,29,0,29,,0,450,,27/01/2026 21:54:47 ,29/01/2026 13:23:15 ,28/01/2026 05:56:33 ,28/01/2026 08:49:49 ,29/01/2026 13:23:15 ," ",,,Fulfillment by seller,คลังหลัก,790647871440,การจัดส่งแบบมาตรฐาน,J&T Express,,d***loutt,D* C*****,(+66)935****92,***** ,ไทย,ประจวบคีรีขันธ์,หัวหิน,,*******************************************************************,,การเก็บเงินปลายทาง,0.3,สเปรย์,1183979357807740682 ,,Unchecked,,,,,,,,,
|
||||
582297882063505309 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435338100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,0,150,150,0,0,150,0,35,0,35,,0,150,,26/01/2026 12:35:00 ,28/01/2026 14:23:18 ,26/01/2026 17:53:06 ,27/01/2026 08:32:47 ,28/01/2026 14:23:18 ," ",,,Fulfillment by seller,คลังหลัก,790542729841,การจัดส่งแบบมาตรฐาน,J&T Express,,n***1238,เ***** น*******,(+66)865****99,***** ,ไทย,นนทบุรี,บางใหญ่,,***********************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1183806770460526493 ,,Unchecked,,,,,,,,,
|
||||
582269992424474572 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,42,0,42,,0,185,,24/01/2026 17:22:10 ,24/01/2026 17:22:37 ,25/01/2026 08:31:20 ,27/01/2026 08:32:47 ,28/01/2026 14:10:28 ," ",,,Fulfillment by seller,คลังหลัก,790425826971,การจัดส่งแบบมาตรฐาน,J&T Express,,a***obe2,ป******* ป*********,(+66)081*****82,***** ,ไทย,กรุงเทพฯ,จอมทอง,,*******************************,,Mbanking,0.12,สเปรย์,1183624685663782860 ,,Unchecked,,,,,,,,,
|
||||
582254854361679673 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333333436664180 ,P-SKU-OZO-JASMINE,aroma oil ขนาด 85ml กลิ่น มะลิ ใช้กับเครื่องพ่นอโรม่า,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,23/01/2026 17:52:44 ,25/01/2026 11:44:10 ,23/01/2026 18:26:31 ,24/01/2026 11:04:52 ,25/01/2026 11:44:10 ," ",,,Fulfillment by seller,คลังหลัก,790466259273,การจัดส่งแบบมาตรฐาน,J&T Express,,a***ee.dvgdv,A***** D****,(+66)062*****40,***** ,ไทย,กรุงเทพฯ,สวนหลวง,,********************************************************************,,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1183516868756211513 ,,Unchecked,,,,,,,,,
|
||||
582245289304425738 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,35,0,35,,0,150,,23/01/2026 02:23:23 ,23/01/2026 02:23:25 ,23/01/2026 06:05:27 ,23/01/2026 13:25:38 ,24/01/2026 17:19:37 ," ",,,Fulfillment by seller,คลังหลัก,790339144840,การจัดส่งแบบมาตรฐาน,J&T Express,,n***uda430,โ***,(+66)635****11,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************************************,*******************,TrueMoney,0.1,สเปรย์,1183434760222508298 ,,Unchecked,,,,,,,,,
|
||||
582179715754133424 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,0,35,0,35,,0,150,,18/01/2026 19:26:21 ,20/01/2026 17:07:14 ,19/01/2026 05:56:59 ,19/01/2026 08:36:49 ,20/01/2026 17:07:14 ," ",,,Fulfillment by seller,คลังหลัก,790146750691,การจัดส่งแบบมาตรฐาน,J&T Express,,u***75684092478776,ล**,(+66)092*****91,***** ,ไทย,กรุงเทพฯ,ประเวศ,,*************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1182956964359735216 ,,Unchecked,,,,,,,,,
|
||||
582153266282399137 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,12,0,188,38,38,0,0,,0,790,,17/01/2026 04:31:43 ,17/01/2026 04:32:59 ,17/01/2026 06:07:59 ,17/01/2026 12:43:29 ,18/01/2026 12:54:41 ," ",,,Fulfillment by seller,คลังหลัก,790047742750,การจัดส่งแบบมาตรฐาน,J&T Express,,n***i804,ร** บ********,(+66)918****71,***** ,ไทย,นครราชสีมา,ปากช่อง,,*********************************************************************,,Mbanking,0.1,สเปรย์,1182776516653188513 ,,Unchecked,,,,,,,,,
|
||||
582153266282399137 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112706932 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,200,200,12,0,188,38,38,0,0,,0,790,,17/01/2026 04:31:43 ,17/01/2026 04:32:59 ,17/01/2026 06:07:59 ,17/01/2026 12:43:29 ,18/01/2026 12:54:41 ," ",,,Fulfillment by seller,คลังหลัก,790047742750,การจัดส่งแบบมาตรฐาน,J&T Express,,n***i804,ร** บ********,(+66)918****71,***** ,ไทย,นครราชสีมา,ปากช่อง,,*********************************************************************,,Mbanking,0.1,สเปรย์,1182776516653188513 ,,Unchecked,,,,,,,,,
|
||||
582153266282399137 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169182425614708 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โรสเมรี่,1,0,200,200,12,0,188,38,38,0,0,,0,790,,17/01/2026 04:31:43 ,17/01/2026 04:32:59 ,17/01/2026 06:07:59 ,17/01/2026 12:43:29 ,18/01/2026 12:54:41 ," ",,,Fulfillment by seller,คลังหลัก,790047742750,การจัดส่งแบบมาตรฐาน,J&T Express,,n***i804,ร** บ********,(+66)918****71,***** ,ไทย,นครราชสีมา,ปากช่อง,,*********************************************************************,,Mbanking,0.1,สเปรย์,1182776516653188513 ,,Unchecked,,,,,,,,,
|
||||
582153266282399137 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169182425942388 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะกรูด,1,0,200,200,12,0,188,38,38,0,0,,0,790,,17/01/2026 04:31:43 ,17/01/2026 04:32:59 ,17/01/2026 06:07:59 ,17/01/2026 12:43:29 ,18/01/2026 12:54:41 ," ",,,Fulfillment by seller,คลังหลัก,790047742750,การจัดส่งแบบมาตรฐาน,J&T Express,,n***i804,ร** บ********,(+66)918****71,***** ,ไทย,นครราชสีมา,ปากช่อง,,*********************************************************************,,Mbanking,0.1,สเปรย์,1182776516653188513 ,,Unchecked,,,,,,,,,
|
||||
582148280844191706 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,27,0,27,,0,150,,16/01/2026 19:30:11 ,18/01/2026 17:17:33 ,17/01/2026 06:06:20 ,17/01/2026 12:43:27 ,18/01/2026 17:17:33 ," ",,,Fulfillment by seller,คลังหลัก,790074841765,การจัดส่งแบบมาตรฐาน,J&T Express,,n***aywin98,D** L**,(+66)823****69,***** ,Thailand,Samut Sakhon,Mueang Samut Sakhon,,********************************,************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1182752405298579418 ,,Unchecked,,,,,,,,,
|
||||
582142615236675097 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,0,88,0,88,,0,200,,16/01/2026 12:27:15 ,18/01/2026 14:23:49 ,16/01/2026 13:16:48 ,16/01/2026 13:34:02 ,18/01/2026 14:23:49 ," ",,,Fulfillment by seller,คลังหลัก,790049357580,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ii554409,พ******* ส*****,(+66)096*****08,***** ,ไทย,เทศบาลนครสุราษฎร์ธานี,เกาะสมุย,,************************************,************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1182708315298498073 ,,Unchecked,,,,,,,,,
|
||||
582072467000231588 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333385252309364 ,P-SKU-OZO-RATRI,น้ำมันอโรม่า กลิ่น ราตรี มีให้เลือกหลายขนาด สร้างบรรยากาศผ่อนคลายในห้อง,30 ml,1,0,79,79,0,0,79,0,35,0,35,0.07,0,78.93,,11/01/2026 21:08:29 ,11/01/2026 21:08:31 ,12/01/2026 05:55:23 ,12/01/2026 08:27:06 ,13/01/2026 16:22:04 ," ",,,Fulfillment by seller,คลังหลัก,769200537884,การจัดส่งแบบมาตรฐาน,J&T Express,,n***659,ท******** น********,(+66)081*****98,***** ,ไทย,กรุงเทพฯ,ลาดกระบัง,,***********************************************,,PayLater,0.12,สเปรย์,1182183757981648548 ,,Unchecked,,,,,,,,,
|
||||
582067655101089705 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,150,300,0,0,300,0,88,0,88,,0,300,,11/01/2026 15:45:08 ,15/01/2026 13:37:03 ,11/01/2026 17:13:01 ,12/01/2026 08:27:07 ,15/01/2026 13:37:03 ," ",,,Fulfillment by seller,คลังหลัก,769202079660,การจัดส่งแบบมาตรฐาน,J&T Express,,u***118054170769,จ***** ป*******,(+66)081*****04,***** ,ไทย,กระบี่,เมืองกระบี่,,********************************************************************,,การเก็บเงินปลายทาง,0.2,สเปรย์,1182156964464265129 ,,Unchecked,,,,,,,,,
|
||||
582040013411026569 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,300,,09/01/2026 19:50:16 ,12/01/2026 14:51:59 ,10/01/2026 08:42:20 ,10/01/2026 11:30:05 ,12/01/2026 14:51:59 ," ",,,Fulfillment by seller,คลังหลัก,769225704030,การจัดส่งแบบมาตรฐาน,J&T Express,,m***htet7394,M** H***,(+66)911****37,***** ,ထိုင်း,Samut Sakhon,Mueang Samut Sakhon,,**************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1181976687471068809 ,,Unchecked,,,,,,,,,
|
||||
582040013411026569 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,300,,09/01/2026 19:50:16 ,12/01/2026 14:51:59 ,10/01/2026 08:42:20 ,10/01/2026 11:30:05 ,12/01/2026 14:51:59 ," ",,,Fulfillment by seller,คลังหลัก,769225704030,การจัดส่งแบบมาตรฐาน,J&T Express,,m***htet7394,M** H***,(+66)911****37,***** ,ထိုင်း,Samut Sakhon,Mueang Samut Sakhon,,**************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1181976687471068809 ,,Unchecked,,,,,,,,,
|
||||
582007406164150076 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,2,0,150,300,0,0,300,0,38,0,38,,0,300,,07/01/2026 17:26:40 ,09/01/2026 13:08:15 ,08/01/2026 06:40:51 ,08/01/2026 08:43:43 ,09/01/2026 13:08:15 ," ",,,Fulfillment by seller,คลังหลัก,769043490880,การจัดส่งแบบมาตรฐาน,J&T Express,,f***.fang5838,ฟ**,(+66)910****66,***** ,ไทย,นครนายก,เมืองนครนายก,,****************************************************************,,การเก็บเงินปลายทาง,0.2,สเปรย์,1181741554583897916 ,,Unchecked,,,,,,,,,
|
||||
582005660587427438 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357657460 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกจำปา,1,0,150,150,0,0,150,0,38,0,38,,0,150,,07/01/2026 15:09:25 ,10/01/2026 13:39:37 ,07/01/2026 15:47:22 ,08/01/2026 08:43:43 ,10/01/2026 13:39:37 ," ",,,Fulfillment by seller,คลังหลัก,769051650732,การจัดส่งแบบมาตรฐาน,J&T Express,,u***83571266775292,u*****************,(+66)616****35,***** ,ไทย,นครราชสีมา,เมืองนครราชสีมา,,********************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1181726823706822254 ,,Unchecked,,,,,,,,,
|
||||
582001711975531937 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,2,0,150,300,0,0,300,0,35,0,35,,0,300,,07/01/2026 10:14:08 ,07/01/2026 10:14:22 ,07/01/2026 15:46:13 ,08/01/2026 08:43:43 ,09/01/2026 11:08:02 ," ",,,Fulfillment by seller,คลังหลัก,769080651732,การจัดส่งแบบมาตรฐาน,J&T Express,,j***ruk1234,ห***** ท******,(+66)948****88,***** ,ไทย,ปทุมธานี,ลำลูกกา,,********************************************************************************************,,Mbanking,0.2,สเปรย์,1181692218939835809 ,,Unchecked,,,,,,,,,
|
||||
582000994560607781 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732135005533014388 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ลูกพีช สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,8,88,0,80,,0,373,,07/01/2026 09:18:32 ,10/01/2026 18:23:43 ,07/01/2026 15:46:54 ,08/01/2026 08:43:44 ,10/01/2026 18:23:43 ," ",,,Fulfillment by seller,คลังหลัก,769028761793,การจัดส่งแบบมาตรฐาน,J&T Express,,k***zaa_,ด****** น******,(+66)098*****42,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*********************************************************************,,การเก็บเงินปลายทาง,0.1,สเปรย์,1181685590491694629 ,,Unchecked,,,,,,,,,
|
||||
582000994560607781 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,8,88,0,80,,0,373,,07/01/2026 09:18:32 ,10/01/2026 18:23:43 ,07/01/2026 15:46:54 ,08/01/2026 08:43:44 ,10/01/2026 18:23:43 ," ",,,Fulfillment by seller,คลังหลัก,769028761793,การจัดส่งแบบมาตรฐาน,J&T Express,,k***zaa_,ด****** น******,(+66)098*****42,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*********************************************************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1181685590491694629 ,,Unchecked,,,,,,,,,
|
||||
581989868554322952 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134087248808308 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ยูคาลิปตัส สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,06/01/2026 14:33:30 ,09/01/2026 11:18:50 ,06/01/2026 15:04:00 ,07/01/2026 15:10:33 ,09/01/2026 11:18:50 ," ",,,Fulfillment by seller,คลังหลัก,769028126300,การจัดส่งแบบมาตรฐาน,J&T Express,,w***ndum,D** S***,(+66)810****35,***** ,ไทย,แพร่,เด่นชัย,,****,*****************************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1181608975003714568 ,,Unchecked,,,,,,,,,
|
||||
581853612746704804 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334796470644 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,0,0,185,0,35,0,35,,0,185,,28/12/2025 07:15:37 ,28/12/2025 07:15:40 ,28/12/2025 08:08:06 ,28/12/2025 09:21:08 ,29/12/2025 14:22:24 ," ",,,Fulfillment by seller,คลังหลัก,768537882733,การจัดส่งแบบมาตรฐาน,J&T Express,,p***339,ป***** ศ***********,(+66)944****46,***** ,ราชอาณาจักรไทย,นนทบุรี,บางใหญ่,,********************************************************************,,TrueMoney,0.12,สเปรย์,1180648997015226276 ,,Unchecked,,,,,,,,,
|
||||
581846926635467816 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133968855861620 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น แคนตาลูป สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,27/12/2025 18:02:40 ,29/12/2025 14:02:05 ,27/12/2025 19:14:28 ,28/12/2025 09:21:06 ,29/12/2025 14:02:05 ," ",,,Fulfillment by seller,คลังหลัก,768560362695,การจัดส่งแบบมาตรฐาน,J&T Express,,t***s.kky,u****************,(+66)933****78,***** ,ไทย,ชลบุรี,สัตหีบ,,**********************************************************************************************,,การเก็บเงินปลายทาง,0.1,น้ำมันหอมระเหย,1180613523931694120 ,,Unchecked,,,,,,,,,
|
||||
581844176616129897 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134080240912756 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกโมก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,4.54,0,74.46,0,38,0,38,,0,148.93,,27/12/2025 14:22:04 ,29/12/2025 22:33:24 ,27/12/2025 16:04:06 ,28/12/2025 09:21:08 ,29/12/2025 22:33:24 ," ",,,Fulfillment by seller,คลังหลัก,768570623704,การจัดส่งแบบมาตรฐาน,J&T Express,,p***hlaing612,p************,(+66)094*****64,***** ,Thailand,Samut Sakhon,Mueang Samut Sakhon,,****************************************************,****************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1180590598378259817 ,,Unchecked,,,,,,,,,
|
||||
581844176616129897 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249184628 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,4.53,0,74.47,0,38,0,38,,0,148.93,,27/12/2025 14:22:04 ,29/12/2025 22:33:24 ,27/12/2025 16:04:06 ,28/12/2025 09:21:08 ,29/12/2025 22:33:24 ," ",,,Fulfillment by seller,คลังหลัก,768570623704,การจัดส่งแบบมาตรฐาน,J&T Express,,p***hlaing612,p************,(+66)094*****64,***** ,Thailand,Samut Sakhon,Mueang Samut Sakhon,,****************************************************,****************************************************,การเก็บเงินปลายทาง,0.1,สเปรย์,1180590598378259817 ,,Unchecked,,,,,,,,,
|
||||
581820551634847619 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,35,35,0,0,,0,185,,25/12/2025 22:57:14 ,27/12/2025 13:51:47 ,26/12/2025 06:13:50 ,26/12/2025 08:40:02 ,27/12/2025 13:51:47 ," ",,,Fulfillment by seller,คลังหลัก,768480074795,การจัดส่งแบบมาตรฐาน,J&T Express,,u***16372418,พ********,(+66)089*****73,***** ,ไทย,ปทุมธานี,เมืองปทุมธานี,,***,**************,การเก็บเงินปลายทาง,0.1,สเปรย์,1180410454976006019 ,,Unchecked,,,,,,,,,
|
||||
581802143209523171 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256170890612 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น โรสเมรี่,1,0,79,79,0,0,79,18,18,0,0,,0,176,,24/12/2025 17:32:48 ,27/12/2025 20:14:40 ,24/12/2025 19:19:06 ,26/12/2025 08:40:01 ,27/12/2025 20:14:40 ," ",,,Fulfillment by seller,คลังหลัก,768493795134,การจัดส่งแบบมาตรฐาน,J&T Express,,k***24,W* l*** b******,(+66)061*****32,***** ,Thailand,Bangkok,Suan Luang,,***********************************,*******,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1180295518938826723 ,,Unchecked,,,,,,,,,
|
||||
581802143209523171 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256170169716 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น โอเชี่ยน,1,0,79,79,0,0,79,18,18,0,0,,0,176,,24/12/2025 17:32:48 ,27/12/2025 20:14:40 ,24/12/2025 19:19:06 ,26/12/2025 08:40:01 ,27/12/2025 20:14:40 ," ",,,Fulfillment by seller,คลังหลัก,768493795134,การจัดส่งแบบมาตรฐาน,J&T Express,,k***24,W* l*** b******,(+66)061*****32,***** ,Thailand,Bangkok,Suan Luang,,***********************************,*******,การเก็บเงินปลายทาง,0.12,น้ำมันหอมระเหย,1180295518938826723 ,,Unchecked,,,,,,,,,
|
||||
581795608888706125 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,60,88,0,28,,0,139,,24/12/2025 08:23:20 ,26/12/2025 21:52:53 ,24/12/2025 10:34:41 ,24/12/2025 12:40:37 ,26/12/2025 21:52:53 ," ",,,Fulfillment by seller,คลังหลัก,768399794956,การจัดส่งแบบมาตรฐาน,J&T Express,,w***man1986,อ***** จ********,(+66)800****92,***** ,ไทย,ภูเก็ต,เมืองภูเก็ต,,******************************,,การเก็บเงินปลายทาง,0.12,สเปรย์,1180238779929560141 ,,Unchecked,,,,,,,,,
|
||||
581772062459659904 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908520308 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,22/12/2025 15:51:29 ,22/12/2025 15:52:24 ,22/12/2025 19:29:13 ,23/12/2025 08:25:26 ,24/12/2025 14:36:25 ," ",,,Fulfillment by seller,คลังหลัก,768364129120,การจัดส่งแบบมาตรฐาน,J&T Express,,s***phet1568,น******,(+66)094*****96,***** ,ไทย,สมุทรปราการ,บางพลี,,*********************************************************************,,PromptPay,0.1,น้ำมันหอมระเหย,1180072237106169472 ,,Unchecked,,,,,,,,,
|
||||
581672979963807113 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807846260 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,15/12/2025 21:53:51 ,15/12/2025 21:53:53 ,16/12/2025 05:55:06 ,16/12/2025 08:52:03 ,17/12/2025 17:15:28 ," ",,,Fulfillment by seller,คลังหลัก,767944455670,การจัดส่งแบบมาตรฐาน,J&T Express,,u***871936185,ป** ‘******,(+66)093*****54,***** ,ไทย,กรุงเทพฯ,จตุจักร,,*************************************************************************************************************,,TrueMoney,0.12,สเปรย์,1179359358790174089 ,,Unchecked,,,,,,,,,
|
||||
581668701516695472 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,38,0,38,,0,300,,15/12/2025 17:00:55 ,18/12/2025 21:09:09 ,15/12/2025 17:21:13 ,16/12/2025 08:50:38 ,18/12/2025 21:09:09 ," ",,,Fulfillment by seller,คลังหลัก,767978244526,การจัดส่งแบบมาตรฐาน,J&T Express,,n***nooknooknook1588,แ*****,(+66)822****59,***** ,ไทย,เลย,ท่าลี่,,******************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1179333159224182704 ,,Unchecked,,,,,,,,,
|
||||
581668701516695472 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,0,38,0,38,,0,300,,15/12/2025 17:00:55 ,18/12/2025 21:09:09 ,15/12/2025 17:21:13 ,16/12/2025 08:50:38 ,18/12/2025 21:09:09 ," ",,,Fulfillment by seller,คลังหลัก,767978244526,การจัดส่งแบบมาตรฐาน,J&T Express,,n***nooknooknook1588,แ*****,(+66)822****59,***** ,ไทย,เลย,ท่าลี่,,******************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1179333159224182704 ,,Unchecked,,,,,,,,,
|
||||
581646325082326646 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134728508803444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกการะเวก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,14/12/2025 08:25:08 ,16/12/2025 20:17:59 ,14/12/2025 09:41:17 ,14/12/2025 10:23:38 ,16/12/2025 20:17:59 ," ",,,Fulfillment by seller,คลังหลัก,767823244790,การจัดส่งแบบมาตรฐาน,J&T Express,,p***itta.63,พ***** ว*******,(+66)092*****61,***** ,ไทย,ปทุมธานี,คลองหลวง,,********************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1179165971042567798 ,,Unchecked,,,,,,,,,
|
||||
581640925975774785 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,38,38,0,0,,0,188,,13/12/2025 20:42:54 ,16/12/2025 15:35:41 ,14/12/2025 07:10:00 ,14/12/2025 10:23:39 ,16/12/2025 15:35:41 ," ",,,Fulfillment by seller,คลังหลัก,767800641635,การจัดส่งแบบมาตรฐาน,J&T Express,,u***6340537323928,ภ******* ก********,(+66)946****96,***** ,ไทย,ชลบุรี,พนัสนิคม,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1179135799522526785 ,,Unchecked,,,,,,,,,
|
||||
581624031027233840 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333333436795252 ,P-SKU-OZO-JASMINE,aroma oil ขนาด 85ml กลิ่น มะลิ ใช้กับเครื่องพ่นอโรม่า,85 ml,1,0,180,180,11.81,0,168.19,0,35,0,35,,0,168.19,,12/12/2025 20:43:58 ,12/12/2025 20:44:28 ,13/12/2025 06:01:53 ,13/12/2025 10:33:03 ,15/12/2025 20:12:53 ," ",,,Fulfillment by seller,คลังหลัก,767886661043,การจัดส่งแบบมาตรฐาน,J&T Express,,t***ok.comuser2nvbkh4ss,ต******,(+66)815****85,***** ,ไทย,กรุงเทพฯ,ลาดพร้าว,,*************************************************************************,************************,Mbanking,0.12,เครื่องหอมในบ้าน,1179009767493567536 ,,Unchecked,,,,,,,,,
|
||||
581623451041498160 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732333333436795252 ,P-SKU-OZO-JASMINE,aroma oil ขนาด 85ml กลิ่น มะลิ ใช้กับเครื่องพ่นอโรม่า,85 ml,1,1,180,180,11.81,0,168.19,0,35,0,35,,0,168.19,168.19,12/12/2025 20:24:29 ," "," "," "," ",12/12/2025 20:26:01 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,t***ok.comuser2nvbkh4ss,ต******,(+66)815****85,***** ,ไทย,กรุงเทพฯ,ลาดพร้าว,,*************************************************************************,************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1179007392988824624 ,,Unchecked,,,,,,,,,
|
||||
581584969848816859 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134568192084340 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กล้วย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,10/12/2025 20:24:54 ,10/12/2025 20:24:58 ,11/12/2025 06:01:27 ,11/12/2025 08:27:50 ,13/12/2025 13:58:01 ," ",,,Fulfillment by seller,คลังหลัก,767640042842,การจัดส่งแบบมาตรฐาน,J&T Express,,k***kwansroinarin,k****************,(+66)918****43,***** ,ไทย,เชียงใหม่,ฝาง,,*********************************,*************,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1178742588746007771 ,,Unchecked,,,,,,,,,
|
||||
581514225241655231 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,0,88,0,88,,0,450,,06/12/2025 13:41:28 ,11/12/2025 19:11:44 ,07/12/2025 18:31:37 ,08/12/2025 08:51:20 ,11/12/2025 19:11:44 ," ",,,Fulfillment by seller,คลังหลัก,767507337026,การจัดส่งแบบมาตรฐาน,J&T Express,,h***2,A** C*** k*** โ***************,(+66)628****15,***** ,Thailand,Surat Thani,Ko Pha-ngan,,***************************************************,*******,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1178259730428102591 ,,Unchecked,,,,,,,,,
|
||||
581514225241655231 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435338100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,0,150,150,0,0,150,0,88,0,88,,0,450,,06/12/2025 13:41:28 ,11/12/2025 19:11:44 ,07/12/2025 18:31:37 ,08/12/2025 08:51:20 ,11/12/2025 19:11:44 ," ",,,Fulfillment by seller,คลังหลัก,767507337026,การจัดส่งแบบมาตรฐาน,J&T Express,,h***2,A** C*** k*** โ***************,(+66)628****15,***** ,Thailand,Surat Thani,Ko Pha-ngan,,***************************************************,*******,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1178259730428102591 ,,Unchecked,,,,,,,,,
|
||||
581514225241655231 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435600244 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกแก้ว,1,0,150,150,0,0,150,0,88,0,88,,0,450,,06/12/2025 13:41:28 ,11/12/2025 19:11:44 ,07/12/2025 18:31:37 ,08/12/2025 08:51:20 ,11/12/2025 19:11:44 ," ",,,Fulfillment by seller,คลังหลัก,767507337026,การจัดส่งแบบมาตรฐาน,J&T Express,,h***2,A** C*** k*** โ***************,(+66)628****15,***** ,Thailand,Surat Thani,Ko Pha-ngan,,***************************************************,*******,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1178259730428102591 ,,Unchecked,,,,,,,,,
|
||||
581508861173925769 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334796274036 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,06/12/2025 07:09:09 ,08/12/2025 11:46:18 ,06/12/2025 08:37:05 ,06/12/2025 08:46:27 ,08/12/2025 11:46:18 ," ",,,Fulfillment by seller,คลังหลัก,767425536371,การจัดส่งแบบมาตรฐาน,J&T Express,,u***8837881513169,ว**** จ****,(+66)085*****92,***** ,ไทย,สระแก้ว,วังน้ำเย็น,,***************************************************************,*****************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1178214552472684425 ,,Unchecked,,,,,,,,,
|
||||
581469384472233355 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,0,38,0,38,,0,150,,03/12/2025 15:27:15 ,05/12/2025 18:44:40 ,03/12/2025 17:39:23 ,04/12/2025 11:39:28 ,05/12/2025 18:44:40 ," ",,,Fulfillment by seller,คลังหลัก,767370696201,การจัดส่งแบบมาตรฐาน,J&T Express,,p***watphonkamchonchal,อ*** ป********,(+66)091*****79,***** ,ไทย,อุทัยธานี,หนองฉาง,,***********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1177949188240541067 ,,Unchecked,,,,,,,,,
|
||||
581460725470103392 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334796470644 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,185,,02/12/2025 23:29:06 ,05/12/2025 16:48:20 ,03/12/2025 05:56:35 ,03/12/2025 09:26:11 ,05/12/2025 16:48:20 ," ",,,Fulfillment by seller,คลังหลัก,767285380783,การจัดส่งแบบมาตรฐาน,J&T Express,,s***nta.n,ร**************,(+66)612****41,***** ,ไทย,พังงา,ท้ายเหมือง,,**************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1177879841000752992 ,,Unchecked,,,,,,,,,
|
||||
581453512786871776 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732135005532817780 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ลูกพีช สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,0,38,0,38,,0,79,,02/12/2025 14:43:08 ,02/12/2025 14:43:28 ,02/12/2025 15:53:40 ,03/12/2025 09:26:12 ,05/12/2025 10:30:03 ," ",,,Fulfillment by seller,คลังหลัก,767264454611,การจัดส่งแบบมาตรฐาน,J&T Express,,k***aph31,k********,(+66)098*****05,***** ,ไทย,อุทัยธานี,ทัพทัน,,******************,,Mbanking,0.1,เครื่องหอมในบ้าน,1177835645006284256 ,,Unchecked,,,,,,,,,
|
||||
581361959317702237 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134499099314548 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกบัว สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,26/11/2025 12:26:30 ,28/11/2025 16:37:35 ,26/11/2025 13:48:30 ,27/11/2025 08:40:37 ,28/11/2025 16:37:35 ," ",,,Fulfillment by seller,คลังหลัก,766993842440,การจัดส่งแบบมาตรฐาน,J&T Express,,m***awaluk,ก*******,(+66)818****58,***** ,ไทย,กรุงเทพฯ,ประเวศ,,*********************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1177201295215986269 ,,Unchecked,,,,,,,,,
|
||||
581345999016920646 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,35,0,35,,0,185,,25/11/2025 10:24:08 ,26/11/2025 15:13:38 ,25/11/2025 11:04:48 ,25/11/2025 11:57:32 ,26/11/2025 15:13:38 ," ",,,Fulfillment by seller,คลังหลัก,766848603931,การจัดส่งแบบมาตรฐาน,J&T Express,,u***88348302873258,y* c***,(+66)652****39,***** ,ไทย,ปทุมธานี,ลำลูกกา,,*****************************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1177090403553936966 ,,Unchecked,,,,,,,,,
|
||||
581260682621716120 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124609396 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,185,,19/11/2025 07:45:44 ,21/11/2025 13:51:29 ,19/11/2025 09:06:56 ,19/11/2025 10:29:31 ,21/11/2025 13:51:29 ," ",,,Fulfillment by seller,คลังหลัก,766653864152,การจัดส่งแบบมาตรฐาน,J&T Express,,u***9744852824040,น***** ส****,(+66)065*****47,***** ,ไทย,นครศรีธรรมราช,ท่าศาลา,,***************************************************************,*************************************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1176516809269610136 ,,Unchecked,,,,,,,,,
|
||||
581257792811795851 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435796852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ส้ม,1,0,150,150,0,0,150,0,38,0,38,,0,150,,18/11/2025 22:26:07 ,20/11/2025 13:55:26 ,19/11/2025 07:04:40 ,19/11/2025 10:29:30 ,20/11/2025 13:55:26 ," ",,,Fulfillment by seller,คลังหลัก,766694640051,การจัดส่งแบบมาตรฐาน,J&T Express,,p***watphonkamchonchal,อ*** ป********,(+66)091*****79,***** ,ไทย,อุทัยธานี,หนองฉาง,,***********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1176499722734372235 ,,Unchecked,,,,,,,,,
|
||||
581250629663425931 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435338100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,150,,18/11/2025 12:52:22 ,21/11/2025 13:04:45 ,18/11/2025 18:22:46 ,19/11/2025 10:29:29 ,21/11/2025 13:04:45 ," ",,,Fulfillment by seller,คลังหลัก,766582045972,การจัดส่งแบบมาตรฐาน,J&T Express,,p***watphonkamchonchal,อ*** ป********,(+66)091*****79,***** ,ไทย,อุทัยธานี,หนองฉาง,,***********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1176453388477367691 ,,Unchecked,,,,,,,,,
|
||||
581241708943803441 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188882804 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,370,,17/11/2025 19:15:54 ,17/11/2025 19:16:12 ,18/11/2025 18:22:10 ,19/11/2025 10:29:30 ,21/11/2025 15:03:11 ," ",,,Fulfillment by seller,คลังหลัก,766581249976,การจัดส่งแบบมาตรฐาน,J&T Express,,m***alaayong,ม** ด**,(+66)080*****37,***** ,ไทย,นครพนม,บ้านแพง,,*************************************************,,Mbanking,0.12,เครื่องหอมในบ้าน,1176394989987464241 ,,Unchecked,,,,,,,,,
|
||||
581241708943803441 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334796470644 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,370,,17/11/2025 19:15:54 ,17/11/2025 19:16:12 ,18/11/2025 18:22:10 ,19/11/2025 10:29:30 ,21/11/2025 15:03:11 ," ",,,Fulfillment by seller,คลังหลัก,766581249976,การจัดส่งแบบมาตรฐาน,J&T Express,,m***alaayong,ม** ด**,(+66)080*****37,***** ,ไทย,นครพนม,บ้านแพง,,*************************************************,,Mbanking,0.12,เครื่องหอมในบ้าน,1176394989987464241 ,,Unchecked,,,,,,,,,
|
||||
581239070642963572 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334796339572 ,P-SKU-OZO-EUCALYPTUS,น้ำหอมอโรมาพ่น กลิ่น ยูคาลิปตัส ขนาด 120ml ปรับอารมณ์ให้ดีขึ้น,60 ml,1,0,150,150,0,0,150,0,38,0,38,,0,150,,17/11/2025 15:28:02 ,21/11/2025 17:12:14 ,18/11/2025 18:21:10 ,19/11/2025 10:29:29 ,21/11/2025 17:12:14 ," ",,,Fulfillment by seller,คลังหลัก,766520743936,การจัดส่งแบบมาตรฐาน,J&T Express,,s***hanthankoratcurlthe2,ม****** เ***********,(+66)081*****23,***** ,ราชอาณาจักรไทย,เทศบาลนครนครราชสีมา,เมืองนครราชสีมา,,*************************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1176375638105228404 ,,Unchecked,,,,,,,,,
|
||||
581222680739218539 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,16/11/2025 11:37:22 ,17/11/2025 13:50:49 ,16/11/2025 11:41:09 ,16/11/2025 16:14:22 ,17/11/2025 13:50:49 ," ",,,Fulfillment by seller,คลังหลัก,766416158885,การจัดส่งแบบมาตรฐาน,J&T Express,,p***anuch879,ป******* ว*********,(+66)615****32,***** ,ไทย,กรุงเทพฯ,บางรัก,,***************************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1176259475256869995 ,,Unchecked,,,,,,,,,
|
||||
581210050141980074 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124609396 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,120 ml,2,0,185,370,0,0,370,0,88,0,88,,0,370,,15/11/2025 12:46:01 ,18/11/2025 13:35:53 ,15/11/2025 17:40:03 ,16/11/2025 16:14:23 ,18/11/2025 13:35:53 ," ",,,Fulfillment by seller,คลังหลัก,766446445682,การจัดส่งแบบมาตรฐาน,J&T Express,,u***54560373848393,พ*****,(+66)967****21,***** ,ไทย,กระบี่,เมืองกระบี่,,***********************************************,********************************************************,การเก็บเงินปลายทาง,0.24,เครื่องหอมในบ้าน,1176181434656261546 ,,Unchecked,,,,,,,,,
|
||||
581209119761073926 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435927924 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แคนตาลูป,1,0,150,150,0,0,150,0,35,0,35,,0,150,,15/11/2025 11:31:37 ,16/11/2025 18:49:32 ,15/11/2025 11:52:05 ,15/11/2025 12:44:49 ,16/11/2025 18:49:32 ," ",,,Fulfillment by seller,คลังหลัก,766444922526,การจัดส่งแบบมาตรฐาน,J&T Express,,p***karaoke,น****,(+66)094*****62,***** ,Thailand,กรุงเทพมหานคร,เขตบางเขน,,*****************************************,*********************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1176174537790293766 ,,Unchecked,,,,,,,,,
|
||||
581087784047183442 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,0,35,0,35,,0,79,,07/11/2025 11:43:31 ,08/11/2025 12:30:26 ,07/11/2025 11:56:42 ,07/11/2025 12:35:34 ,08/11/2025 12:30:26 ," ",,,Fulfillment by seller,คลังหลัก,766039156606,การจัดส่งแบบมาตรฐาน,J&T Express,,a***d47,อ****,(+66)065*****49,***** ,ไทย,กรุงเทพฯ,หนองแขม,,**************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1175361332107052626 ,,Unchecked,,,,,,,,,
|
||||
581086738148918852 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,35,0,35,,0,185,,07/11/2025 10:24:21 ,07/11/2025 10:24:22 ,07/11/2025 11:55:10 ,07/11/2025 12:35:34 ,08/11/2025 15:58:12 ," ",,,Fulfillment by seller,คลังหลัก,766055462605,การจัดส่งแบบมาตรฐาน,J&T Express,,t***on572,ข******** ไ*****,(+66)085*****21,***** ,ไทย,กรุงเทพมหานคร,บางบอน,,*******************************************************************************************,************************,PayLater,0.12,เครื่องหอมในบ้าน,1175353474639300164 ,,Unchecked,,,,,,,,,
|
||||
581049718109013980 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332589807988 ,P-SKU-OZO-GRAPE-SCENT,น้ำมันหอมระเหย ขนาด 120ml กลิ่น องุ่น อโรม่าแท้ 100%,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,117,,04/11/2025 21:42:05 ,08/11/2025 14:29:07 ,05/11/2025 06:05:47 ,05/11/2025 08:19:46 ,08/11/2025 14:29:07 ," ",,,Fulfillment by seller,คลังหลัก,765910464443,การจัดส่งแบบมาตรฐาน,J&T Express,,r***konmai,ร******* อ*****,(+66)063*****54,***** ,ไทย,เทศบาลเมืองมุกดาหาร,เมืองมุกดาหาร,,*************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1175105622367700956 ,,Unchecked,,,,,,,,,
|
||||
581049714359699279 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134529418430836 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกปีบ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,36,0,36,,0,180,,04/11/2025 21:38:16 ,07/11/2025 21:31:09 ,05/11/2025 06:05:05 ,05/11/2025 08:19:43 ,07/11/2025 21:31:09 ," ",,,Fulfillment by seller,คลังหลัก,765928062432,การจัดส่งแบบมาตรฐาน,J&T Express,,d***eginning,ส****** ท*******,(+66)065*****40,***** ,ไทย,ชัยภูมิ,เทพสถิต,,**************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1175105460943226703 ,,Unchecked,,,,,,,,,
|
||||
581025791964120966 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134499099314548 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกบัว สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,03/11/2025 09:30:16 ,05/11/2025 13:41:05 ,03/11/2025 10:15:58 ,03/11/2025 10:34:11 ,05/11/2025 13:41:05 ," ",,,Fulfillment by seller,คลังหลัก,765849960621,การจัดส่งแบบมาตรฐาน,J&T Express,,r***anakornjunim,เ** จ****** โ************* ร******,(+66)081*****98,***** ,ไทย,สุโขทัย,สวรรคโลก,,*****************,***********,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1174936756575373190 ,,Unchecked,,,,,,,,,
|
||||
581023448832378362 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484624628084 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกลิลลี่,1,0,200,200,0,0,200,0,35,0,35,,0,200,,03/11/2025 03:55:01 ,04/11/2025 17:07:49 ,03/11/2025 06:20:43 ,03/11/2025 10:34:11 ,04/11/2025 17:07:49 ," ",,,Fulfillment by seller,คลังหลัก,765810592363,การจัดส่งแบบมาตรฐาน,J&T Express,,c***gchanthorn09,C*************,(+66)946****27,***** ,Thailand,Bangkok,Bang Rak,,***************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1174911508924761594 ,,Unchecked,,,,,,,,,
|
||||
580965033886451377 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,0,38,0,38,,0,185,,29/10/2025 20:55:55 ,01/11/2025 22:19:41 ,30/10/2025 05:58:16 ,30/10/2025 08:35:54 ,01/11/2025 22:19:41 ," ",,,Fulfillment by seller,คลังหลัก,765670439520,การจัดส่งแบบมาตรฐาน,J&T Express,,b***s55,น********* ส********,(+66)637****98,***** ,ไทย,ชัยนาท,เมืองชัยนาท,,***************************************,******,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1174537179011974833 ,,Unchecked,,,,,,,,,
|
||||
580930688998082473 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,2,0,150,300,0,0,300,0,88,0,88,,0,300,,27/10/2025 11:06:11 ,30/10/2025 02:19:58 ,27/10/2025 11:21:13 ,27/10/2025 12:54:07 ,30/10/2025 02:19:58 ," ",,,Fulfillment by seller,คลังหลัก,765582204343,การจัดส่งแบบมาตรฐาน,J&T Express,,u***118054170769,จ***** ป*******,(+66)081*****04,***** ,ไทย,กระบี่,เมืองกระบี่,,********************************************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1174297984722110377 ,,Unchecked,,,,,,,,,
|
||||
580905723518944682 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,150,300,0,0,300,0,88,0,88,,0,300,,25/10/2025 13:44:55 ,29/10/2025 21:33:11 ,26/10/2025 18:52:15 ,27/10/2025 08:43:41 ,29/10/2025 21:33:11 ," ",,,Fulfillment by seller,คลังหลัก,765439418951,การจัดส่งแบบมาตรฐาน,J&T Express,,u***54560373848393,พ*****,(+66)967****21,***** ,ไทย,กระบี่,เมืองกระบี่,,***********************************************,********************************************************,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1174143263962924458 ,,Unchecked,,,,,,,,,
|
||||
580894089821456000 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908323700 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,1,30,0,29,,0,80,,24/10/2025 16:37:24 ,24/10/2025 16:38:21 ,24/10/2025 19:13:52 ,25/10/2025 09:30:05 ,26/10/2025 10:35:49 ," ",,,Fulfillment by seller,คลังหลัก,765437251110,การจัดส่งแบบมาตรฐาน,J&T Express,,s***phet1568,น******,(+66)094*****96,***** ,ไทย,สมุทรปราการ,บางพลี,,*********************************************************************,,PromptPay,0.1,เครื่องหอมในบ้าน,1174069775986689664 ,,Unchecked,,,,,,,,,
|
||||
580864715424565219 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,150,150,0,0,150,0,38,0,38,,0,150,,22/10/2025 11:49:55 ,25/10/2025 20:50:54 ,22/10/2025 18:52:09 ,23/10/2025 12:58:58 ,25/10/2025 20:50:54 ," ",,,Fulfillment by seller,คลังหลัก,765310768386,การจัดส่งแบบมาตรฐาน,J&T Express,,b***y.waraphorn,โ*** ว******,(+66)095*****06,***** ,ไทย,ศรีสะเกษ,ขุขันธ์,,****************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1173867795048073187 ,,Unchecked,,,,,,,,,
|
||||
580855649264502390 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134728508803444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกการะเวก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,35,0,35,,0,180,,21/10/2025 17:11:38 ,23/10/2025 14:30:19 ,21/10/2025 17:43:26 ,22/10/2025 12:23:24 ,23/10/2025 14:30:19 ," ",,,Fulfillment by seller,คลังหลัก,765249285942,การจัดส่งแบบมาตรฐาน,J&T Express,,p***itta.63,พ***** ว*******,(+66)092*****61,***** ,ไทย,ปทุมธานี,คลองหลวง,,********************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1173812232280704630 ,,Unchecked,,,,,,,,,
|
||||
580779510402548856 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333353125906804 ,P-SKU-OZO-POMEGRANATE,น้ำมันอโรม่า ขนาดมาตรฐาน ใช้ได้นาน กลิ่น ทับทิม กลิ่นหอมธรรมชาติ,120 ml,1,0,185,185,0,0,185,0,35,0,35,,0,185,,15/10/2025 15:10:13 ,17/10/2025 14:31:07 ,15/10/2025 19:26:03 ,16/10/2025 08:39:44 ,17/10/2025 14:31:07 ," ",,,Fulfillment by seller,คลังหลัก,765016384613,การจัดส่งแบบมาตรฐาน,J&T Express,,p***ng81,พ****** ผ******,(+66)941****14,***** ,ไทย,กรุงเทพฯ,บางกะปิ,,***********************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1173306975407604856 ,,Unchecked,,,,,,,,,
|
||||
580753830509643197 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908520308 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,13/10/2025 15:56:01 ,13/10/2025 15:56:47 ,13/10/2025 17:14:26 ,14/10/2025 08:18:02 ,16/10/2025 10:29:51 ," ",,,Fulfillment by seller,คลังหลัก,764904270756,การจัดส่งแบบมาตรฐาน,J&T Express,,h***oru3,น***** แ*****,(+66)971****28,***** ,ไทย,ศรีสะเกษ,เมืองศรีสะเกษ,,**********************************************,,PromptPay,0.1,เครื่องหอมในบ้าน,1173132246622635453 ,,Unchecked,,,,,,,,,
|
||||
580690277845796465 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133222530582900 ,,Hom Aroma น้ำมันหอมระเหย กลิ่นตะไคร้บ้าน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,0,130,130,0,0,130,0,35,0,35,,0,130,,08/10/2025 21:35:46 ,10/10/2025 17:37:49 ,09/10/2025 07:14:42 ,09/10/2025 08:52:39 ,10/10/2025 17:37:49 ," ",,,Fulfillment by seller,คลังหลัก,764720495781,การจัดส่งแบบมาตรฐาน,J&T Express,,g***7267,N** p*****,(+66)081*****49,***** ,Thái Lan,Bangkok,Bang Sue,,********************************************,**************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172717445499684465 ,,Unchecked,,,,,,,,,
|
||||
580686328153737143 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807911796 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,60 ml,1,0,150,150,0,0,150,0,38,0,38,,0,150,,08/10/2025 15:58:33 ,08/10/2025 15:58:35 ,08/10/2025 19:17:16 ,09/10/2025 08:52:39 ,10/10/2025 10:49:13 ," ",,,Fulfillment by seller,คลังหลัก,764777328744,การจัดส่งแบบมาตรฐาน,J&T Express,,a***0121,อ***** แ***** เ********* โ****,(+66)098*****14,***** ,ไทย,ระยอง,บ้านฉาง,,*******************************************************************************,,PayLater,0.12,เครื่องหอมในบ้าน,1172695391770216375 ,,Unchecked,,,,,,,,,
|
||||
580673932853806152 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623645044 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,200,200,0,0,200,0,88,0,88,,0,400,,07/10/2025 16:26:20 ,10/10/2025 15:52:23 ,07/10/2025 16:27:58 ,08/10/2025 12:10:21 ,10/10/2025 15:52:23 ," ",,,Fulfillment by seller,คลังหลัก,764779180270,การจัดส่งแบบมาตรฐาน,J&T Express,,l***htetluu,T*** P****,(+66)094*****49,***** ,Thailand,Surat Thani,Ko Samui,,***************************************************************************,****************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172610767557657672 ,,Unchecked,,,,,,,,,
|
||||
580673932853806152 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484624824692 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,200,200,0,0,200,0,88,0,88,,0,400,,07/10/2025 16:26:20 ,10/10/2025 15:52:23 ,07/10/2025 16:27:58 ,08/10/2025 12:10:21 ,10/10/2025 15:52:23 ," ",,,Fulfillment by seller,คลังหลัก,764779180270,การจัดส่งแบบมาตรฐาน,J&T Express,,l***htetluu,T*** P****,(+66)094*****49,***** ,Thailand,Surat Thani,Ko Samui,,***************************************************************************,****************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172610767557657672 ,,Unchecked,,,,,,,,,
|
||||
580623405739443728 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,8,88,0,80,,0,158,,03/10/2025 15:54:55 ,06/10/2025 15:37:21 ,03/10/2025 20:05:35 ,04/10/2025 11:15:51 ,06/10/2025 15:37:21 ," ",,,Fulfillment by seller,คลังหลัก,764589966781,การจัดส่งแบบมาตรฐาน,J&T Express,,p***12525,ป***** ห****,(+66)092*****38,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,**************************************************,*************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172281990441240080 ,,Unchecked,,,,,,,,,
|
||||
580622611328763515 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134087249004916 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ยูคาลิปตัส สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,0,38,0,38,,0,180,,03/10/2025 14:38:41 ,03/10/2025 14:39:15 ,03/10/2025 20:04:45 ,04/10/2025 11:15:51 ,06/10/2025 14:31:27 ," ",,,Fulfillment by seller,คลังหลัก,764585048776,การจัดส่งแบบมาตรฐาน,J&T Express,,h***ap8,h******,(+66)095*****65,***** ,ไทย,อุบลราชธานี,สิรินธร,,*************************************************************************************************************,,PromptPay,0.1,เครื่องหอมในบ้าน,1172274644067059323 ,,Unchecked,,,,,,,,,
|
||||
580603117105808386 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,8,88,0,80,,0,158,,01/10/2025 22:46:46 ,04/10/2025 13:36:05 ,02/10/2025 06:24:56 ,02/10/2025 08:20:26 ,04/10/2025 13:36:05 ," ",,,Fulfillment by seller,คลังหลัก,764453063920,การจัดส่งแบบมาตรฐาน,J&T Express,,u***p05sbj5j8v,Z**,(+66)639****30,***** ,ไทย,ภูเก็ต,เมืองภูเก็ต,,*************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172136709056922626 ,,Unchecked,,,,,,,,,
|
||||
580598540876022950 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,1,150,150,0,0,150,0,35,0,35,,0,150,150,01/10/2025 16:16:00 ," "," "," "," ",01/10/2025 20:23:01 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,e***esso298.2044,ส***** บ***,(+66)085*****24,***** ,ไทย,กรุงเทพฯ,บางกะปิ,,***************************************************************,*****************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1172111844124951718 ,,Unchecked,,,,,,,,,
|
||||
580522486329739174 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,35,35,0,0,,0,635,,25/09/2025 19:37:24 ,28/09/2025 14:56:42 ,26/09/2025 07:42:11 ,26/09/2025 14:05:52 ,28/09/2025 14:56:42 ," ",,,Fulfillment by seller,คลังหลัก,764208326604,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1171625027665954726 ,,Unchecked,,,,,,,,,
|
||||
580522486329739174 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435600244 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกแก้ว,2,0,150,300,0,0,300,35,35,0,0,,0,635,,25/09/2025 19:37:24 ,28/09/2025 14:56:42 ,26/09/2025 07:42:11 ,26/09/2025 14:05:52 ,28/09/2025 14:56:42 ," ",,,Fulfillment by seller,คลังหลัก,764208326604,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1171625027665954726 ,,Unchecked,,,,,,,,,
|
||||
580522486329739174 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436190068 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โรสเมรี่,1,0,150,150,0,0,150,35,35,0,0,,0,635,,25/09/2025 19:37:24 ,28/09/2025 14:56:42 ,26/09/2025 07:42:11 ,26/09/2025 14:05:52 ,28/09/2025 14:56:42 ," ",,,Fulfillment by seller,คลังหลัก,764208326604,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1171625027665954726 ,,Unchecked,,,,,,,,,
|
||||
580432639675958312 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435796852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ส้ม,1,0,150,150,0,0,150,35,35,0,0,,0,185,,18/09/2025 14:03:10 ,20/09/2025 17:27:52 ,19/09/2025 07:28:33 ,19/09/2025 10:52:03 ,20/09/2025 17:27:52 ," ",,,Fulfillment by seller,คลังหลัก,763989572006,การจัดส่งแบบมาตรฐาน,J&T Express,,j***llchannon,J***** c******,(+66)099*****16,***** ,ไทย,กรุงเทพฯ,ทวีวัฒนา,,*********************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1171042008821040168 ,,Unchecked,,,,,,,,,
|
||||
580380255024023261 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134195930499444 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น เปปเปอร์มินต์ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,35,35,0,0,,0,215,,14/09/2025 10:25:50 ,16/09/2025 17:09:02 ,14/09/2025 18:22:35 ,15/09/2025 09:03:08 ,16/09/2025 17:09:02 ," ",,,Fulfillment by seller,คลังหลัก,763883967361,การจัดส่งแบบมาตรฐาน,J&T Express,,n***8043,น***********,(+66)061*****70,***** ,ไทย,กรุงเทพฯ,ราชเทวี,,******************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1170692027945354973 ,,Unchecked,,,,,,,,,
|
||||
580354685117301892 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333353125906804 ,P-SKU-OZO-POMEGRANATE,น้ำมันอโรม่า ขนาดมาตรฐาน ใช้ได้นาน กลิ่น ทับทิม กลิ่นหอมธรรมชาติ,120 ml,1,0,185,185,0,0,185,35,35,0,0,,0,220,,12/09/2025 10:37:09 ,13/09/2025 11:22:55 ,12/09/2025 11:24:40 ,12/09/2025 12:36:17 ,13/09/2025 11:22:55 ," ",,,Fulfillment by seller,คลังหลัก,763762060574,การจัดส่งแบบมาตรฐาน,J&T Express,,m***anaratputtidach,S********,(+66)834****67,***** ,ไทย,ปทุมธานี,ธัญบุรี,,************************************************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1170533962470884484 ,,Unchecked,,,,,,,,,
|
||||
580302852141123497 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,88,88,0,0,,0,238,,08/09/2025 14:51:06 ,11/09/2025 19:24:04 ,09/09/2025 06:11:46 ,09/09/2025 12:25:33 ,11/09/2025 19:24:04 ," ",,,Fulfillment by seller,คลังหลัก,763614103070,การจัดส่งแบบมาตรฐาน,J&T Express,,u***118054170769,จ***** ป*******,(+66)081*****04,***** ,ไทย,กระบี่,เมืองกระบี่,,********************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1170187056070494121 ,,Unchecked,,,,,,,,,
|
||||
580275068729591475 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,2,0,150,300,0,0,300,35,35,0,0,,0,335,,06/09/2025 12:38:07 ,10/09/2025 17:18:51 ,07/09/2025 16:56:06 ,08/09/2025 12:16:11 ,10/09/2025 17:18:51 ," ",,,Fulfillment by seller,คลังหลัก,763506352383,การจัดส่งแบบมาตรฐาน,J&T Express,,t***_baitoey21,น****** อ****,(+66)098*****17,***** ,ไทย,นนทบุรี,บางบัวทอง,,**********************************************,***********************,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1170013023133337267 ,,Unchecked,,,,,,,,,
|
||||
580250675975128933 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807846260 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,117,,04/09/2025 14:15:22 ,08/09/2025 12:30:20 ,05/09/2025 10:36:30 ,05/09/2025 12:43:10 ,08/09/2025 12:30:20 ," ",,,Fulfillment by seller,คลังหลัก,763407595565,การจัดส่งแบบมาตรฐาน,J&T Express,,u***71068504,น*********** ต*******,(+66)957****05,***** ,ไทย,อุดรธานี,กุมภวาปี,,***************************************************************************,*,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1169859479459825509 ,,Unchecked,,,,,,,,,
|
||||
580220414042015570 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,79,79,0,0,79,38,38,0,0,,0,117,,02/09/2025 07:39:03 ,03/09/2025 13:31:04 ,02/09/2025 09:22:20 ,02/09/2025 11:35:19 ,03/09/2025 13:31:04 ," ",,,Fulfillment by seller,คลังหลัก,763351896322,การจัดส่งแบบมาตรฐาน,J&T Express,,p***ornjaiaue,ภ**** ใ******,(+66)098*****50,***** ,ไทย,เทศบาลนครนครสวรรค์,ท่าตะโก,,***************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169654292562413394 ,,Unchecked,,,,,,,,,
|
||||
580210838215624388 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134953158084980 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น องุ่น สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,35,35,0,0,,0,114,,01/09/2025 13:10:38 ,01/09/2025 13:11:31 ,01/09/2025 17:09:24 ,02/09/2025 11:34:54 ,03/09/2025 15:39:05 ," ",,,Fulfillment by seller,คลังหลัก,763391444196,การจัดส่งแบบมาตรฐาน,J&T Express,,a***sit1985,T** A****** K*******,(+66)834****72,***** ,ไทย,นนทบุรี,ปากเกร็ด,,************************************,*****************************************,Mbanking,0.1,เครื่องหอมในบ้าน,1169599091419219652 ,,Unchecked,,,,,,,,,
|
||||
580207221420623207 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333346190100852 ,P-SKU-OZO-VANILLA,oil ozone ขนาด 30ml กลิ่น วานิลลา ใช้ในห้องนอน ห้องนั่งเล่น,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,196,,01/09/2025 07:47:32 ,02/09/2025 18:36:50 ,01/09/2025 08:30:05 ,01/09/2025 12:01:16 ,02/09/2025 18:36:50 ," ",,,Fulfillment by seller,คลังหลัก,763241186820,การจัดส่งแบบมาตรฐาน,J&T Express,,w***1456756,โ************,(+66)088*****25,***** ,ไทย,ระยอง,เมืองระยอง,,*************,*************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1169567468170478951 ,,Unchecked,,,,,,,,,
|
||||
580207221420623207 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333386395650420 ,P-SKU-OZO-COFFEE,น้ำมันหอมอโรมา กลิ่น กาแฟ ขนาด 120ml ใช้ในบ้านและรถยนต์,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,196,,01/09/2025 07:47:32 ,02/09/2025 18:36:50 ,01/09/2025 08:30:05 ,01/09/2025 12:01:16 ,02/09/2025 18:36:50 ," ",,,Fulfillment by seller,คลังหลัก,763241186820,การจัดส่งแบบมาตรฐาน,J&T Express,,w***1456756,โ************,(+66)088*****25,***** ,ไทย,ระยอง,เมืองระยอง,,*************,*************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1169567468170478951 ,,Unchecked,,,,,,,,,
|
||||
580180195276588067 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146113231220 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เซเว่นซี,1,0,200,200,0,0,200,30,35,0,5,,0,430,,30/08/2025 00:01:28 ,31/08/2025 16:45:45 ,30/08/2025 07:08:08 ,30/08/2025 08:25:17 ,31/08/2025 16:45:45 ," ",,,Fulfillment by seller,คลังหลัก,763214953071,การจัดส่งแบบมาตรฐาน,J&T Express,,r***kaeosap,เ*** แ*********,(+66)916****46,***** ,ไทย,ปทุมธานี,ธัญบุรี,,***,************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169408454318916643 ,,Unchecked,,,,,,,,,
|
||||
580180195276588067 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146113100148 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,200,200,0,0,200,30,35,0,5,,0,430,,30/08/2025 00:01:28 ,31/08/2025 16:45:45 ,30/08/2025 07:08:08 ,30/08/2025 08:25:17 ,31/08/2025 16:45:45 ," ",,,Fulfillment by seller,คลังหลัก,763214953071,การจัดส่งแบบมาตรฐาน,J&T Express,,r***kaeosap,เ*** แ*********,(+66)916****46,***** ,ไทย,ปทุมธานี,ธัญบุรี,,***,************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169408454318916643 ,,Unchecked,,,,,,,,,
|
||||
580157578715694946 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357264244 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกปีบ,2,0,150,300,0,0,300,38,38,0,0,,0,338,,28/08/2025 08:12:03 ,31/08/2025 11:35:23 ,29/08/2025 10:55:05 ,29/08/2025 12:28:27 ,31/08/2025 11:35:23 ," ",,,Fulfillment by seller,คลังหลัก,763153621871,การจัดส่งแบบมาตรฐาน,J&T Express,,u***71808338198408,เ******** ด******,(+66)081*****19,***** ,ไทย,พิษณุโลก,วังทอง,,****************************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1169259266285209442 ,,Unchecked,,,,,,,,,
|
||||
580157137719100534 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,0,38,0,38,,0,150,,28/08/2025 07:18:51 ,31/08/2025 14:14:56 ,29/08/2025 10:54:29 ,29/08/2025 12:28:28 ,31/08/2025 14:14:56 ," ",,,Fulfillment by seller,คลังหลัก,763131224870,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rudee,ส***** ,(+66)844****46,***** ,ไทย,นครศรีธรรมราช,นครศรีธรรมราช,,**************************************************,*********,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169254824260306038 ,,Unchecked,,,,,,,,,
|
||||
580143425139803290 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,35,35,0,0,,0,185,,27/08/2025 00:01:38 ,28/08/2025 13:24:56 ,27/08/2025 06:47:57 ,27/08/2025 10:04:55 ,28/08/2025 13:24:56 ," ",,,Fulfillment by seller,คลังหลัก,763044886855,การจัดส่งแบบมาตรฐาน,J&T Express,,s***warat,น***************** ว****************,(+66)946****66,***** ,ไทย,กรุงเทพฯ,หนองแขม,,*************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169164747428103322 ,,Unchecked,,,,,,,,,
|
||||
580134011332494373 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,38,38,0,0,,0,238,,26/08/2025 09:48:54 ,28/08/2025 17:26:34 ,26/08/2025 10:06:54 ,26/08/2025 15:08:33 ,28/08/2025 17:26:34 ," ",,,Fulfillment by seller,คลังหลัก,763080782704,การจัดส่งแบบมาตรฐาน,J&T Express,,l***900,ล**,(+66)099*****32,***** ,ไทย,มุกดาหาร,เมืองมุกดาหาร,,*****************,***************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1169107631931294757 ,,Unchecked,,,,,,,,,
|
||||
580133640832386085 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,200,400,0,0,400,38,38,0,0,,0,438,,26/08/2025 09:14:54 ,28/08/2025 17:06:25 ,26/08/2025 10:06:54 ,26/08/2025 15:08:32 ,28/08/2025 17:06:25 ," ",,,Fulfillment by seller,คลังหลัก,763040982701,การจัดส่งแบบมาตรฐาน,J&T Express,,l***900,ล**,(+66)099*****32,***** ,ไทย,มุกดาหาร,เมืองมุกดาหาร,,*****************,***************,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1169104667621688357 ,,Unchecked,,,,,,,,,
|
||||
580124913037640997 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357002100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กลิ่นหมากฝรั่ง,4,0,150,600,0,0,600,35,35,0,0,,0,635,,25/08/2025 15:00:06 ,25/08/2025 15:00:43 ,25/08/2025 16:23:18 ,26/08/2025 15:08:32 ,27/08/2025 12:08:32 ," ",,,Fulfillment by seller,คลังหลัก,763066464462,การจัดส่งแบบมาตรฐาน,J&T Express,,a***5m,O*** m***,(+66)946****72,***** ,Thailand,Nonthaburi,Mueang Nonthaburi,,***************************************************************************************************,,PromptPay,0.4,เครื่องหอมในบ้าน,1169051437989266725 ,,Unchecked,,,,,,,,,
|
||||
580116110497187393 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357067636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ผลไม้รวม,1,0,150,150,0,0,150,38,38,0,0,,0,188,,24/08/2025 20:49:17 ,26/08/2025 20:23:32 ,25/08/2025 06:01:29 ,25/08/2025 12:51:19 ,26/08/2025 20:23:32 ," ",,,Fulfillment by seller,คลังหลัก,763098557141,การจัดส่งแบบมาตรฐาน,J&T Express,,u***6340537323928,ภ******* ก********,(+66)946****96,***** ,ไทย,ชลบุรี,พนัสนิคม,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168989746216404545 ,,Unchecked,,,,,,,,,
|
||||
580092563685278855 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807846260 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,30 ml,2,0,79,158,0,0,158,35,35,0,0,,0,193,,22/08/2025 22:18:50 ,22/08/2025 22:18:54 ,23/08/2025 06:30:09 ,23/08/2025 08:48:41 ,25/08/2025 11:52:15 ," ",,,Fulfillment by seller,คลังหลัก,762973066586,การจัดส่งแบบมาตรฐาน,J&T Express,,k***.wannarumol,อ**** ว******,(+66)859****41,***** ,ไทย,นนทบุรี,บางบัวทอง,,********************************,*************************************************************************************************************************,บัตรเครดิต/เดบิต,0.24,เครื่องหอมในบ้าน,1168856431057536135 ,,Unchecked,,,,,,,,,
|
||||
580087146079159734 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,150,150,0,0,150,35,35,0,0,,0,185,,22/08/2025 14:13:24 ,25/08/2025 14:52:08 ,23/08/2025 06:30:09 ,23/08/2025 08:48:41 ,25/08/2025 14:52:08 ," ",,,Fulfillment by seller,คลังหลัก,762943867582,การจัดส่งแบบมาตรฐาน,J&T Express,,f***ver7608,ณ****** ล***,(+66)092*****88,***** ,ไทย,กรุงเทพฯ,วังทองหลาง,,**********************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168825073656366518 ,,Unchecked,,,,,,,,,
|
||||
580079444768228522 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623907188 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,200,200,0,0,200,38,38,0,0,,0,238,,21/08/2025 20:57:09 ,24/08/2025 11:44:49 ,22/08/2025 07:08:10 ,22/08/2025 12:47:24 ,24/08/2025 11:44:49 ," ",,,Fulfillment by seller,คลังหลัก,762905747251,การจัดส่งแบบมาตรฐาน,J&T Express,,u***76205317692618,ช******* เ********,(+66)641****85,***** ,ไทย,มุกดาหาร,หว้านใหญ่,,*************,*,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168775447990338730 ,,Unchecked,,,,,,,,,
|
||||
580078958873053173 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,38,38,0,0,,0,188,,21/08/2025 20:16:22 ,24/08/2025 15:01:19 ,22/08/2025 07:07:30 ,22/08/2025 12:47:26 ,24/08/2025 15:01:19 ," ",,,Fulfillment by seller,คลังหลัก,762959846246,การจัดส่งแบบมาตรฐาน,J&T Express,,u***6609803150213,ส***** ป********,(+66)961****42,***** ,ไทย,พังงา,ตะกั่วป่า,,**************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168773602612905973 ,,Unchecked,,,,,,,,,
|
||||
580072468392674881 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,150,300,0,0,300,30,35,0,5,,0,480,,21/08/2025 10:24:14 ,21/08/2025 10:24:54 ,22/08/2025 07:06:14 ,22/08/2025 12:47:24 ,23/08/2025 15:21:15 ," ",,,Fulfillment by seller,คลังหลัก,762929945245,การจัดส่งแบบมาตรฐาน,J&T Express,,p***t_pp25,พ************ ช***********,(+66)616****69,***** ,ไทย,สมุทรปราการ,บางบ่อ,,******************************************************************************************,*************************************************************,Mbanking,0.2,เครื่องหอมในบ้าน,1168728649485878849 ,,Unchecked,,,,,,,,,
|
||||
580072468392674881 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,30,35,0,5,,0,480,,21/08/2025 10:24:14 ,21/08/2025 10:24:54 ,22/08/2025 07:06:14 ,22/08/2025 12:47:24 ,23/08/2025 15:21:15 ," ",,,Fulfillment by seller,คลังหลัก,762929945245,การจัดส่งแบบมาตรฐาน,J&T Express,,p***t_pp25,พ************ ช***********,(+66)616****69,***** ,ไทย,สมุทรปราการ,บางบ่อ,,******************************************************************************************,*************************************************************,Mbanking,0.1,เครื่องหอมในบ้าน,1168728649485878849 ,,Unchecked,,,,,,,,,
|
||||
580062650294109704 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,38,38,0,0,,0,188,,20/08/2025 14:00:55 ,24/08/2025 12:29:39 ,21/08/2025 06:31:33 ,21/08/2025 08:36:10 ,24/08/2025 12:29:39 ," ",,,Fulfillment by seller,คลังหลัก,762854996842,การจัดส่งแบบมาตรฐาน,J&T Express,,a***gasconboiser,A*** B*****,(+66)650****35,***** ,Thailand,Nakhon Si Thammarat,Sichon,,*******************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168668090548913672 ,,Unchecked,,,,,,,,,
|
||||
580051054214808675 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,2,0,150,300,0,0,300,33,38,0,5,,0,333,,19/08/2025 14:49:25 ,21/08/2025 15:18:06 ,20/08/2025 06:09:44 ,20/08/2025 12:35:02 ,21/08/2025 15:18:06 ," ",,,Fulfillment by seller,คลังหลัก,762863737580,การจัดส่งแบบมาตรฐาน,J&T Express,,p***a4545,P********,(+66)882****42,***** ,ไทย,ชลบุรี,บางละมุง,,***********************************************************************************************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1168592856761468003 ,,Unchecked,,,,,,,,,
|
||||
580011217913349935 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134087249004916 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ยูคาลิปตัส สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,2,0,180,360,0,0,360,38,38,0,0,,0,398,,16/08/2025 08:08:13 ,18/08/2025 12:07:02 ,16/08/2025 11:57:59 ,16/08/2025 12:49:15 ,18/08/2025 12:07:02 ," ",,,Fulfillment by seller,คลังหลัก,762755634384,การจัดส่งแบบมาตรฐาน,J&T Express,,c***tanaleklersbamrung,ฉ***** เ************,(+66)091*****20,***** ,ไทย,ชัยภูมิ,หนองบัวแดง,,***********************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1168338061480396591 ,,Unchecked,,,,,,,,,
|
||||
580004099532555996 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134226620418420 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ส้ม สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,38,38,0,0,,0,218,,15/08/2025 15:43:15 ,18/08/2025 11:40:02 ,16/08/2025 11:56:17 ,16/08/2025 12:49:16 ,18/08/2025 11:40:02 ," ",,,Fulfillment by seller,คลังหลัก,762761432381,การจัดส่งแบบมาตรฐาน,J&T Express,,u***5413039581149,u****************,(+66)822****29,***** ,ไทย,ลพบุรี,พัฒนานิคม,,*********************************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168300159450318556 ,,Unchecked,,,,,,,,,
|
||||
580003834608911653 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357002100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กลิ่นหมากฝรั่ง,6,0,150,900,47.37,0,852.63,83,88,0,5,,0,935.63,,15/08/2025 15:11:42 ,18/08/2025 12:55:30 ,16/08/2025 11:53:05 ,16/08/2025 12:49:16 ,18/08/2025 12:55:30 ," ",,,Fulfillment by seller,คลังหลัก,762763130404,การจัดส่งแบบมาตรฐาน,J&T Express,,a***5m,e***** a** m***,(+66)062*****37,***** ,Thailand,Phuket,Thalang,,******************************************,,การเก็บเงินปลายทาง,0.6,เครื่องหอมในบ้าน,1168297130288645413 ,,Unchecked,,,,,,,,,
|
||||
579992253167339126 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908454772 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,85 ml,1,0,150,150,0,0,150,35,35,0,0,,0,185,,14/08/2025 15:41:22 ,17/08/2025 11:59:23 ,14/08/2025 17:53:46 ,16/08/2025 09:20:54 ,17/08/2025 11:59:23 ," ",,,Fulfillment by seller,คลังหลัก,762612802814,การจัดส่งแบบมาตรฐาน,J&T Express,,p***itta.63,พ***** ว*******,(+66)092*****61,***** ,ไทย,ปทุมธานี,คลองหลวง,,********************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168221708608702070 ,,Unchecked,,,,,,,,,
|
||||
579977804633900309 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333346190297460 ,P-SKU-OZO-VANILLA,oil ozone ขนาด 30ml กลิ่น วานิลลา ใช้ในห้องนอน ห้องนั่งเล่น,120 ml,5,0,185,925,0,0,925,88,88,0,0,,0,1013,,13/08/2025 11:39:46 ,13/08/2025 11:40:03 ,13/08/2025 12:43:02 ,13/08/2025 13:04:11 ,15/08/2025 14:29:35 ," ",,,Fulfillment by seller,คลังหลัก,762624919490,การจัดส่งแบบมาตรฐาน,J&T Express,,t***yalice,ส****** ช********,(+66)649****99,***** ,ไทย,ภูเก็ต,เมืองภูเก็ต,,*****************************************************************************************,*********,Mbanking,0.6,เครื่องหอมในบ้าน,1168121540600497429 ,,Unchecked,,,,,,,,,
|
||||
579977665269630230 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,200,400,15,0,385,35,35,0,0,,0,420,,13/08/2025 11:28:26 ,14/08/2025 16:45:06 ,13/08/2025 12:40:59 ,13/08/2025 13:04:11 ,14/08/2025 16:45:06 ," ",,,Fulfillment by seller,คลังหลัก,762610015473,การจัดส่งแบบมาตรฐาน,J&T Express,,f***tmmobile,ว***** พ*********,(+66)080*****21,***** ,ไทย,สมุทรปราการ,พระสมุทรเจดีย์,,***************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1168120088100635926 ,,Unchecked,,,,,,,,,
|
||||
579975466336356162 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333329188882804 ,P-SKU-OZO-PEPPERMINT,humidifier oil กลิ่น เปปเปอร์มินต์ ขนาด 85ml บำบัดอารมณ์,120 ml,1,0,185,185,0,0,185,38,38,0,0,,0,223,,13/08/2025 07:47:20 ,15/08/2025 10:46:23 ,13/08/2025 08:09:37 ,13/08/2025 13:04:12 ,15/08/2025 10:46:23 ," ",,,Fulfillment by seller,คลังหลัก,762617055211,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7150922428132,ฐ******** ม******,(+66)093*****95,***** ,ไทย,กาฬสินธุ์,ยางตลาด,,******************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1168100682080880450 ,,Unchecked,,,,,,,,,
|
||||
579964579030533441 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,30,35,0,5,,0,330,,12/08/2025 09:42:56 ,13/08/2025 13:20:07 ,12/08/2025 09:43:38 ,12/08/2025 11:45:57 ,13/08/2025 13:20:07 ," ",,,Fulfillment by seller,คลังหลัก,762682209100,การจัดส่งแบบมาตรฐาน,J&T Express,,m***e192,n* m* m**,(+66)099*****75,***** ,Thailand,Bangkok,Saphan Sung,,************************************************************************************,************************************************************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168033827646637377 ,,Unchecked,,,,,,,,,
|
||||
579964579030533441 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435731316 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลีลาวดี,1,0,150,150,0,0,150,30,35,0,5,,0,330,,12/08/2025 09:42:56 ,13/08/2025 13:20:07 ,12/08/2025 09:43:38 ,12/08/2025 11:45:57 ,13/08/2025 13:20:07 ," ",,,Fulfillment by seller,คลังหลัก,762682209100,การจัดส่งแบบมาตรฐาน,J&T Express,,m***e192,n* m* m**,(+66)099*****75,***** ,Thailand,Bangkok,Saphan Sung,,************************************************************************************,************************************************************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1168033827646637377 ,,Unchecked,,,,,,,,,
|
||||
579958658776728941 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,38,38,0,0,,0,188,,11/08/2025 19:12:56 ,14/08/2025 17:54:38 ,11/08/2025 19:14:24 ,12/08/2025 11:45:57 ,14/08/2025 17:54:38 ," ",,,Fulfillment by seller,คลังหลัก,762596255864,การจัดส่งแบบมาตรฐาน,J&T Express,,u***870291439,ม**** ศ***********,(+66)091*****66,***** ,ไทย,ตรัง,สิเกา,,*********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167999273539634541 ,,Unchecked,,,,,,,,,
|
||||
579955247811102168 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,33,38,0,5,,0,333,,11/08/2025 13:49:52 ,13/08/2025 14:42:38 ,11/08/2025 17:43:23 ,12/08/2025 11:45:56 ,13/08/2025 14:42:38 ," ",,,Fulfillment by seller,คลังหลัก,762543448844,การจัดส่งแบบมาตรฐาน,J&T Express,,3***supinya,ห*************** ฝ*****,(+66)092*****15,***** ,ไทย,นครราชสีมา,เมืองนครราชสีมา,,****************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167973688873682392 ,,Unchecked,,,,,,,,,
|
||||
579955247811102168 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357395316 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอโซน,1,0,150,150,0,0,150,33,38,0,5,,0,333,,11/08/2025 13:49:52 ,13/08/2025 14:42:38 ,11/08/2025 17:43:23 ,12/08/2025 11:45:56 ,13/08/2025 14:42:38 ," ",,,Fulfillment by seller,คลังหลัก,762543448844,การจัดส่งแบบมาตรฐาน,J&T Express,,3***supinya,ห*************** ฝ*****,(+66)092*****15,***** ,ไทย,นครราชสีมา,เมืองนครราชสีมา,,****************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167973688873682392 ,,Unchecked,,,,,,,,,
|
||||
579954775405070243 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,38,38,0,0,,0,488,,11/08/2025 13:03:56 ,11/08/2025 13:04:35 ,11/08/2025 17:41:17 ,12/08/2025 11:45:57 ,13/08/2025 13:54:58 ," ",,,Fulfillment by seller,คลังหลัก,762580743864,การจัดส่งแบบมาตรฐาน,J&T Express,,j***har,y**** a* a**,(+66)925****99,***** ,تايلاند,محافظة تشونبوري,Bang Lamung,,*******************************************************************************************************,,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1167969418755278755 ,,Unchecked,,,,,,,,,
|
||||
579954775405070243 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436124532 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกลิลลี่,1,0,150,150,0,0,150,38,38,0,0,,0,488,,11/08/2025 13:03:56 ,11/08/2025 13:04:35 ,11/08/2025 17:41:17 ,12/08/2025 11:45:57 ,13/08/2025 13:54:58 ," ",,,Fulfillment by seller,คลังหลัก,762580743864,การจัดส่งแบบมาตรฐาน,J&T Express,,j***har,y**** a* a**,(+66)925****99,***** ,تايلاند,محافظة تشونبوري,Bang Lamung,,*******************************************************************************************************,,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1167969418755278755 ,,Unchecked,,,,,,,,,
|
||||
579954775405070243 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357722996 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกพิกุล,1,0,150,150,0,0,150,38,38,0,0,,0,488,,11/08/2025 13:03:56 ,11/08/2025 13:04:35 ,11/08/2025 17:41:17 ,12/08/2025 11:45:57 ,13/08/2025 13:54:58 ," ",,,Fulfillment by seller,คลังหลัก,762580743864,การจัดส่งแบบมาตรฐาน,J&T Express,,j***har,y**** a* a**,(+66)925****99,***** ,تايلاند,محافظة تشونبوري,Bang Lamung,,*******************************************************************************************************,,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1167969418755278755 ,,Unchecked,,,,,,,,,
|
||||
579950398151493201 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333335124412788 ,P-SKU-OZO-LEMONGRASS,น้ำหอมอโรมาพ่น กลิ่น ตะไคร้บ้าน ขนาด 30ml บำบัดอารมณ์,30 ml,1,0,79,79,0,0,79,35,35,0,0,,0,114,,11/08/2025 03:10:07 ,12/08/2025 14:11:46 ,11/08/2025 07:22:02 ,11/08/2025 08:32:18 ,12/08/2025 14:11:46 ," ",,,Fulfillment by seller,คลังหลัก,762549757561,การจัดส่งแบบมาตรฐาน,J&T Express,,f***857,ส**** พ****,(+66)641****05,***** ,ไทย,กรุงเทพฯ,บางขุนเทียน,,**************************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1167930647112877649 ,,Unchecked,,,,,,,,,
|
||||
579928631677585134 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,150,150,0,0,150,88,88,0,0,,0,238,,09/08/2025 09:32:28 ,11/08/2025 14:56:50 ,09/08/2025 10:48:15 ,09/08/2025 11:08:26 ,11/08/2025 14:56:50 ," ",,,Fulfillment by seller,คลังหลัก,762546831126,การจัดส่งแบบมาตรฐาน,J&T Express,,l***hkt,ม**** ม****,(+66)081*****22,***** ,ไทย,ภูเก็ต,ถลาง,,********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167803618322712302 ,,Unchecked,,,,,,,,,
|
||||
579919319126869488 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357133172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะกรูด,2,0,150,300,0,0,300,33,38,0,5,,0,333,,08/08/2025 15:09:24 ,13/08/2025 14:08:23 ,08/08/2025 18:06:26 ,09/08/2025 11:07:17 ,13/08/2025 14:08:23 ," ",,,Fulfillment by seller,คลังหลัก,762435094901,การจัดส่งแบบมาตรฐาน,J&T Express,,v***wan088,ว******* ศ******,(+66)084*****99,***** ,ไทย,กาฬสินธุ์,เมืองกาฬสินธุ์,,***************************************************************************************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1167747876098049520 ,,Unchecked,,,,,,,,,
|
||||
579915239255082018 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,35,35,0,0,,0,185,,08/08/2025 09:48:08 ,11/08/2025 17:03:32 ,08/08/2025 18:05:24 ,09/08/2025 11:07:17 ,11/08/2025 17:03:32 ," ",,,Fulfillment by seller,คลังหลัก,762453290900,การจัดส่งแบบมาตรฐาน,J&T Express,,u***41575437052835,จ******* ว*************,(+66)086*****25,***** ,ไทย,นนทบุรี,บางกรวย,,************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167716223874204706 ,,Unchecked,,,,,,,,,
|
||||
579904846021297334 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436190068 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โรสเมรี่,1,0,150,150,10.5,0,139.5,38,38,0,0,,0,177.5,,07/08/2025 13:51:19 ,10/08/2025 15:58:54 ,07/08/2025 17:27:44 ,09/08/2025 11:07:17 ,10/08/2025 15:58:54 ," ",,,Fulfillment by seller,คลังหลัก,762455441532,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ramassage423656,น**** ด**********,(+66)632****29,***** ,ไทย,นครสวรรค์,เมืองนครสวรรค์,,************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167652264731706550 ,,Unchecked,,,,,,,,,
|
||||
579894237210576659 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357133172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะกรูด,1,0,150,150,15,0,135,38,38,0,0,,0,173,,06/08/2025 16:41:05 ,10/08/2025 11:42:20 ,06/08/2025 18:59:09 ,07/08/2025 13:23:12 ,10/08/2025 11:42:20 ," ",,,Fulfillment by seller,คลังหลัก,762499816223,การจัดส่งแบบมาตรฐาน,J&T Express,,p***alay888,ป****** ว*********,(+66)081*****44,***** ,ราชอาณาจักรไทย,เทศบาลนครขอนแก่น,หนองเรือ,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167586280769160979 ,,Unchecked,,,,,,,,,
|
||||
579887017449653502 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,33,38,0,5,,0,333,,06/08/2025 01:48:31 ,07/08/2025 14:55:46 ,06/08/2025 05:55:41 ,06/08/2025 11:54:35 ,07/08/2025 14:55:46 ," ",,,Fulfillment by seller,คลังหลัก,762339333976,การจัดส่งแบบมาตรฐาน,J&T Express,,d***gchai4,น********** ป*******,(+66)084*****40,***** ,ราชอาณาจักรไทย,ชลบุรี,บางละมุง,,*****************************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167531110255461630 ,,Unchecked,,,,,,,,,
|
||||
579887017449653502 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435665780 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,1,0,150,150,0,0,150,33,38,0,5,,0,333,,06/08/2025 01:48:31 ,07/08/2025 14:55:46 ,06/08/2025 05:55:41 ,06/08/2025 11:54:35 ,07/08/2025 14:55:46 ," ",,,Fulfillment by seller,คลังหลัก,762339333976,การจัดส่งแบบมาตรฐาน,J&T Express,,d***gchai4,น********** ป*******,(+66)084*****40,***** ,ราชอาณาจักรไทย,ชลบุรี,บางละมุง,,*****************************************************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167531110255461630 ,,Unchecked,,,,,,,,,
|
||||
579876320550618896 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,35,35,0,0,,0,185,,05/08/2025 08:05:37 ,05/08/2025 08:05:41 ,05/08/2025 08:17:57 ,05/08/2025 12:31:23 ,06/08/2025 16:39:03 ," ",,,Fulfillment by seller,คลังหลัก,762334354655,การจัดส่งแบบมาตรฐาน,J&T Express,,a***united,S****** S**********,(+66)875****18,***** ,ไทย,Pathum Thani,Mueang Pathum Thani,,**********************,**********************,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1167461326990968592 ,,Unchecked,,,,,,,,,
|
||||
579869607105430910 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,35,35,0,0,,0,185,,04/08/2025 16:24:38 ,06/08/2025 14:21:36 ,04/08/2025 16:52:43 ,05/08/2025 12:31:23 ,06/08/2025 14:21:36 ," ",,,Fulfillment by seller,คลังหลัก,762347916606,การจัดส่งแบบมาตรฐาน,J&T Express,,x***aikeosenephet,เ***,(+66)064*****04,***** ,ไทย,กรุงเทพฯ,ดินแดง,,***************************************************,*******,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167423181211469182 ,,Unchecked,,,,,,,,,
|
||||
579868349817652547 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,33,38,0,5,,0,333,,04/08/2025 14:26:46 ,06/08/2025 15:34:29 ,04/08/2025 16:50:47 ,05/08/2025 12:31:24 ,06/08/2025 15:34:29 ," ",,,Fulfillment by seller,คลังหลัก,762341817580,การจัดส่งแบบมาตรฐาน,J&T Express,,k***nlakp2525,ล*****,(+66)098*****48,***** ,ไทย,ชลบุรี,บางละมุง,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167412598865954115 ,,Unchecked,,,,,,,,,
|
||||
579868349817652547 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,33,38,0,5,,0,333,,04/08/2025 14:26:46 ,06/08/2025 15:34:29 ,04/08/2025 16:50:47 ,05/08/2025 12:31:24 ,06/08/2025 15:34:29 ," ",,,Fulfillment by seller,คลังหลัก,762341817580,การจัดส่งแบบมาตรฐาน,J&T Express,,k***nlakp2525,ล*****,(+66)098*****48,***** ,ไทย,ชลบุรี,บางละมุง,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167412598865954115 ,,Unchecked,,,,,,,,,
|
||||
579855644339046210 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,0,0,150,38,38,0,0,,0,188,,03/08/2025 13:55:24 ,07/08/2025 11:59:48 ,03/08/2025 16:45:58 ,04/08/2025 10:17:08 ,07/08/2025 11:59:48 ," ",,,Fulfillment by seller,คลังหลัก,762390594055,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7150922428132,ฐ******** ม******,(+66)093*****95,***** ,ไทย,กาฬสินธุ์,ยางตลาด,,******************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167328074546251586 ,,Unchecked,,,,,,,,,
|
||||
579842396998435852 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,38,38,0,0,,0,238,,02/08/2025 12:14:21 ,02/08/2025 12:14:43 ,03/08/2025 16:45:04 ,04/08/2025 10:17:08 ,06/08/2025 16:53:32 ," ",,,Fulfillment by seller,คลังหลัก,762327195044,การจัดส่งแบบมาตรฐาน,J&T Express,,n***41994874,ส****** ค****,(+66)838****53,***** ,ไทย,เพชรบูรณ์,เมืองเพชรบูรณ์,,*********************************************************,*********************,Mbanking,0.1,เครื่องหอมในบ้าน,1167250546808882188 ,,Unchecked,,,,,,,,,
|
||||
579833803483939958 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,38,38,0,0,,0,188,,01/08/2025 17:54:33 ,04/08/2025 13:03:58 ,02/08/2025 06:24:36 ,02/08/2025 10:20:05 ,04/08/2025 13:03:58 ," ",,,Fulfillment by seller,คลังหลัก,762277753580,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rudee,ส***** ,(+66)844****46,***** ,ไทย,นครศรีธรรมราช,นครศรีธรรมราช,,**************************************************,*********,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1167198926563214454 ,,Unchecked,,,,,,,,,
|
||||
579792724371867515 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333332807780724 ,P-SKU-OZO-CANTALOUPE,น้ำมันหอมระเหย สมุนไพรอโรม่า ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ใช้กับเครื่อง diffuser,120 ml,1,0,185,185,0,0,185,35,35,0,0,,0,220,,29/07/2025 09:42:39 ,29/07/2025 09:42:41 ,29/07/2025 09:43:25 ,29/07/2025 13:18:00 ,30/07/2025 15:35:01 ," ",,,Fulfillment by seller,คลังหลัก,762162773260,การจัดส่งแบบมาตรฐาน,J&T Express,,n***less7013,ด*****,(+66)094*****19,***** ,ไทย,สมุทรปราการ,บางพลี,,*******************************************************,********,TrueMoney,0.12,เครื่องหอมในบ้าน,1166926570914875259 ,,Unchecked,,,,,,,,,
|
||||
579788772353082730 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,35,35,0,0,,0,235,,28/07/2025 21:53:04 ,30/07/2025 19:11:54 ,29/07/2025 08:33:34 ,29/07/2025 13:17:59 ,30/07/2025 19:11:54 ," ",,,Fulfillment by seller,คลังหลัก,762131108211,การจัดส่งแบบมาตรฐาน,J&T Express,,s***takadka641,S*****,(+66)062*****19,***** ,Thailand,Bangkok,Ratchathewi,,******************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166900999273940330 ,,Unchecked,,,,,,,,,
|
||||
579769308716434695 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435338100 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,0,150,150,0,0,150,88,88,0,0,,0,238,,27/07/2025 11:14:12 ,31/07/2025 19:59:13 ,27/07/2025 17:14:53 ,28/07/2025 13:34:13 ,31/07/2025 19:59:13 ," ",,,Fulfillment by seller,คลังหลัก,762089578666,การจัดส่งแบบมาตรฐาน,J&T Express,,b***boy9358,ณ****** เ**********,(+66)801****45,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,*********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166782412516656391 ,,Unchecked,,,,,,,,,
|
||||
579751128573773334 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134426714016116 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกลิลลี่ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,196,,25/07/2025 19:50:13 ,27/07/2025 13:18:00 ,26/07/2025 06:40:28 ,26/07/2025 12:44:25 ,27/07/2025 13:18:00 ," ",,,Fulfillment by seller,คลังหลัก,762022022300,การจัดส่งแบบมาตรฐาน,J&T Express,,m***uy613,น*** ภ****,(+66)095*****19,***** ,ไทย,สุพรรณบุรี,สองพี่น้อง,,***************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166678371068315158 ,,Unchecked,,,,,,,,,
|
||||
579751128573773334 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333334651111796 ,P-SKU-OZO-OZONE,oil ozone ขนาด 85ml กลิ่น โอโซน ลดกลิ่นอับในห้อง,30 ml,1,0,79,79,0,0,79,38,38,0,0,,0,196,,25/07/2025 19:50:13 ,27/07/2025 13:18:00 ,26/07/2025 06:40:28 ,26/07/2025 12:44:25 ,27/07/2025 13:18:00 ," ",,,Fulfillment by seller,คลังหลัก,762022022300,การจัดส่งแบบมาตรฐาน,J&T Express,,m***uy613,น*** ภ****,(+66)095*****19,***** ,ไทย,สุพรรณบุรี,สองพี่น้อง,,***************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1166678371068315158 ,,Unchecked,,,,,,,,,
|
||||
579749009688331279 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169182426270068 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,200,200,0,0,200,35,35,0,0,,0,235,,25/07/2025 16:35:39 ,25/07/2025 16:36:04 ,25/07/2025 16:59:41 ,26/07/2025 12:44:25 ,27/07/2025 16:46:34 ," ",,,Fulfillment by seller,คลังหลัก,762024562132,การจัดส่งแบบมาตรฐาน,J&T Express,,u***7623350303416,แ*****,(+66)846****52,***** ,ไทย,ปทุมธานี,ลำลูกกา,,******************************************************,,Mbanking,0.1,เครื่องหอมในบ้าน,1166666800720806927 ,,Unchecked,,,,,,,,,
|
||||
579742855684196260 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146113362292 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ดอกโมก,4,0,200,800,0,0,800,38,38,0,0,,0,838,,25/07/2025 06:37:26 ,27/07/2025 14:16:44 ,25/07/2025 07:41:41 ,25/07/2025 11:04:56 ,27/07/2025 14:16:44 ," ",,,Fulfillment by seller,คลังหลัก,761901650974,การจัดส่งแบบมาตรฐาน,J&T Express,,j***jeab50,เ*****,(+66)936****84,***** ,ไทย,ร้อยเอ็ด,เมืองร้อยเอ็ด,,*************************************************************************,,การเก็บเงินปลายทาง,0.4,เครื่องหอมในบ้าน,1166619905414039460 ,,Unchecked,,,,,,,,,
|
||||
579739829497595320 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256170038644 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น เฟรช ลาเวนเดอร์,3,0,79,237,0,0,237,38,38,0,0,,0,275,,24/07/2025 21:10:26 ,26/07/2025 20:18:28 ,25/07/2025 06:19:12 ,25/07/2025 11:04:56 ,26/07/2025 20:18:28 ," ",,,Fulfillment by seller,คลังหลัก,761986547932,การจัดส่งแบบมาตรฐาน,J&T Express,,r***epattoka,ร****** แ******,(+66)800****28,***** ,Thailand,Chachoengsao,Bang Pakong,,****************************************************************************************************************************,,การเก็บเงินปลายทาง,0.36,เครื่องหอมในบ้าน,1166607383294936504 ,,Unchecked,,,,,,,,,
|
||||
579706170261276117 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134529418430836 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกปีบ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,38,38,0,0,,0,218,,21/07/2025 23:51:01 ,23/07/2025 12:38:31 ,22/07/2025 05:59:23 ,22/07/2025 12:47:48 ,23/07/2025 12:38:31 ," ",,,Fulfillment by seller,คลังหลัก,761814365992,การจัดส่งแบบมาตรฐาน,J&T Express,,u***109715639496,u***************,(+66)061*****13,***** ,ไทย,ปราจีนบุรี,เมืองปราจีนบุรี,,************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166401753376851413 ,,Unchecked,,,,,,,,,
|
||||
579701985519699083 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134080241109364 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกโมก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,33,38,0,5,,0,398,,21/07/2025 17:18:52 ,23/07/2025 17:22:01 ,21/07/2025 17:51:07 ,22/07/2025 12:47:48 ,23/07/2025 17:22:01 ," ",,,Fulfillment by seller,คลังหลัก,761849789901,การจัดส่งแบบมาตรฐาน,J&T Express,,o***y4562,อ** ศ****,(+66)654****15,***** ,ไทย,นครปฐม,สามพราน,,*************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166383240248329355 ,,Unchecked,,,,,,,,,
|
||||
579701985519699083 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333417262385524 ,P-SKU-OZO-ORANGE-JASMINE,อโรม่าหอมระเหย ขนาด 85ml กลิ่น ดอกแก้ว กลิ่นหอมสดชื่น,120 ml,1,0,185,185,0,0,185,33,38,0,5,,0,398,,21/07/2025 17:18:52 ,23/07/2025 17:22:01 ,21/07/2025 17:51:07 ,22/07/2025 12:47:48 ,23/07/2025 17:22:01 ," ",,,Fulfillment by seller,คลังหลัก,761849789901,การจัดส่งแบบมาตรฐาน,J&T Express,,o***y4562,อ** ศ****,(+66)654****15,***** ,ไทย,นครปฐม,สามพราน,,*************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1166383240248329355 ,,Unchecked,,,,,,,,,
|
||||
579693621240628985 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,2,0,150,300,0,0,300,38,38,0,0,,0,338,,20/07/2025 22:35:48 ,22/07/2025 13:06:10 ,21/07/2025 06:17:22 ,21/07/2025 09:33:25 ,22/07/2025 13:06:10 ," ",,,Fulfillment by seller,คลังหลัก,761875462530,การจัดส่งแบบมาตรฐาน,J&T Express,,u***nq9bvfd3kh,ธ******** ม********,(+66)085*****43,***** ,ไทย,สระบุรี,มวกเหล็ก,,*****************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1166322344063436537 ,,Unchecked,,,,,,,,,
|
||||
579656918418555975 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,3,3,150,450,0,0,450,0,35,0,35,,0,900,450,17/07/2025 21:28:56 ,17/07/2025 21:29:41 ," "," "," ",20/07/2025 00:15:19 ,System,ยกเลิกอัตโนมัติเนื่องจากเลยเวลารับพัสดุ,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.3,เครื่องหอมในบ้าน,1166124906074047559 ,,Unchecked,,,,,,,,,
|
||||
579656918418555975 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,3,3,150,450,0,0,450,0,35,0,35,,0,900,450,17/07/2025 21:28:56 ,17/07/2025 21:29:41 ," "," "," ",20/07/2025 00:15:19 ,System,ยกเลิกอัตโนมัติเนื่องจากเลยเวลารับพัสดุ,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.3,เครื่องหอมในบ้าน,1166124906074047559 ,,Unchecked,,,,,,,,,
|
||||
579653027910092537 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,2,2,150,300,0,0,300,33,38,0,5,,0,333,300,17/07/2025 15:30:21 ," "," "," "," ",20/07/2025 00:41:11 ,System,ยกเลิกอัตโนมัติเนื่องจากเลยเวลารับพัสดุ,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,u***nq9bvfd3kh,ธ******** ม********,(+66)085*****43,***** ,ไทย,สระบุรี,มวกเหล็ก,,*****************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1166105018342737657 ,,Unchecked,,,,,,,,,
|
||||
579640171907483561 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435403636 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,150,150,0,0,150,88,88,0,0,,0,238,,16/07/2025 14:08:38 ,19/07/2025 19:02:16 ,17/07/2025 06:07:03 ,17/07/2025 08:42:42 ,19/07/2025 19:02:16 ," ",,,Fulfillment by seller,คลังหลัก,761728635430,การจัดส่งแบบมาตรฐาน,J&T Express,,u***118054170769,จ***** ป*******,(+66)081*****04,***** ,ไทย,กระบี่,เมืองกระบี่,,********************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1166027309866715049 ,,Unchecked,,,,,,,,,
|
||||
579625901278069927 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357133172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะกรูด,1,0,150,150,0,0,150,38,38,0,0,,0,188,,15/07/2025 10:15:53 ,17/07/2025 12:29:04 ,15/07/2025 11:28:41 ,15/07/2025 11:49:15 ,17/07/2025 12:29:04 ," ",,,Fulfillment by seller,คลังหลัก,761668198966,การจัดส่งแบบมาตรฐาน,J&T Express,,u***429609136,u****************,(+66)810****44,***** ,ไทย,เลย,เมืองเลย,,*******************************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1165937734375736487 ,,Unchecked,,,,,,,,,
|
||||
579624406185641178 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333346190166388 ,P-SKU-OZO-VANILLA,oil ozone ขนาด 30ml กลิ่น วานิลลา ใช้ในห้องนอน ห้องนั่งเล่น,60 ml,1,0,150,150,22.5,0,127.5,33,38,0,5,,0,355.15,,15/07/2025 07:22:56 ,15/07/2025 07:23:23 ,15/07/2025 08:56:20 ,15/07/2025 10:03:12 ,16/07/2025 14:55:43 ," ",,,Fulfillment by seller,คลังหลัก,761624229984,การจัดส่งแบบมาตรฐาน,J&T Express,,l***.eat.cheese,ช***** ช****,(+66)083*****61,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,**********************************************************************,***********,Mbanking,0.12,เครื่องหอมในบ้าน,1165924432471819482 ,,Unchecked,,,,,,,,,
|
||||
579624406185641178 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333418295626100 ,P-SKU-OZO-SAKURA,น้ำหอมเครื่องพ่น กลิ่น ซากุระ ขนาดมาตรฐาน ใช้ได้นาน เหมาะกับคนรักความหอม,30 ml,1,0,79,79,11.85,0,67.15,33,38,0,5,,0,355.15,,15/07/2025 07:22:56 ,15/07/2025 07:23:23 ,15/07/2025 08:56:20 ,15/07/2025 10:03:12 ,16/07/2025 14:55:43 ," ",,,Fulfillment by seller,คลังหลัก,761624229984,การจัดส่งแบบมาตรฐาน,J&T Express,,l***.eat.cheese,ช***** ช****,(+66)083*****61,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,**********************************************************************,***********,Mbanking,0.12,เครื่องหอมในบ้าน,1165924432471819482 ,,Unchecked,,,,,,,,,
|
||||
579624406185641178 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732135005532948852 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ลูกพีช สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,85 ml,1,0,150,150,22.5,0,127.5,33,38,0,5,,0,355.15,,15/07/2025 07:22:56 ,15/07/2025 07:23:23 ,15/07/2025 08:56:20 ,15/07/2025 10:03:12 ,16/07/2025 14:55:43 ," ",,,Fulfillment by seller,คลังหลัก,761624229984,การจัดส่งแบบมาตรฐาน,J&T Express,,l***.eat.cheese,ช***** ช****,(+66)083*****61,***** ,ไทย,ชลบุรี,เมืองชลบุรี,,**********************************************************************,***********,Mbanking,0.1,เครื่องหอมในบ้าน,1165924432471819482 ,,Unchecked,,,,,,,,,
|
||||
579582841497028224 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908389236 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,0,130,130,0,0,130,35,35,0,0,,0,165,,11/07/2025 14:53:13 ,11/07/2025 14:54:15 ,12/07/2025 07:57:35 ,14/07/2025 08:25:12 ,15/07/2025 14:46:48 ," ",,,Fulfillment by seller,คลังหลัก,761523353964,การจัดส่งแบบมาตรฐาน,J&T Express,,s***phet1568,น******,(+66)094*****96,***** ,ไทย,สมุทรปราการ,บางพลี,,*********************************************************************,,PromptPay,0.1,เครื่องหอมในบ้าน,1165689544861582976 ,,Unchecked,,,,,,,,,
|
||||
579515252397474977 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623513972 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,200,200,0,0,200,35,35,0,0,,0,235,,06/07/2025 07:49:22 ,06/07/2025 07:49:24 ,06/07/2025 16:48:51 ,08/07/2025 15:31:26 ,09/07/2025 13:49:35 ," ",,,Fulfillment by seller,คลังหลัก,761418351304,การจัดส่งแบบมาตรฐาน,J&T Express,,c***on1689,ม*** เ**********,(+66)065*****37,***** ,ไทย,กรุงเทพฯ,สาทร,,*******************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1165296005917541537 ,,Unchecked,,,,,,,,,
|
||||
579504833677658022 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436190068 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โรสเมรี่,2,0,150,300,0,0,300,30,35,0,5,,0,930,,05/07/2025 10:08:26 ,06/07/2025 21:11:17 ,05/07/2025 10:14:18 ,05/07/2025 11:24:43 ,06/07/2025 21:11:17 ," ",,,Fulfillment by seller,คลังหลัก,761358495855,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1165239518260856742 ,,Unchecked,,,,,,,,,
|
||||
579504833677658022 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,2,0,150,300,0,0,300,30,35,0,5,,0,930,,05/07/2025 10:08:26 ,06/07/2025 21:11:17 ,05/07/2025 10:14:18 ,05/07/2025 11:24:43 ,06/07/2025 21:11:17 ," ",,,Fulfillment by seller,คลังหลัก,761358495855,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1165239518260856742 ,,Unchecked,,,,,,,,,
|
||||
579504833677658022 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,2,0,150,300,0,0,300,30,35,0,5,,0,930,,05/07/2025 10:08:26 ,06/07/2025 21:11:17 ,05/07/2025 10:14:18 ,05/07/2025 11:24:43 ,06/07/2025 21:11:17 ," ",,,Fulfillment by seller,คลังหลัก,761358495855,การจัดส่งแบบมาตรฐาน,J&T Express,,s***rthaimassage77,ว******** ฮ*******,(+66)955****61,***** ,ไทย,กรุงเทพฯ,วัฒนา,,*****************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1165239518260856742 ,,Unchecked,,,,,,,,,
|
||||
579502686427973598 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249184628 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,88,88,0,0,,0,167,,05/07/2025 04:27:32 ,05/07/2025 04:30:17 ,05/07/2025 06:34:14 ,05/07/2025 11:24:43 ,07/07/2025 13:13:25 ," ",,,Fulfillment by seller,คลังหลัก,761347274726,การจัดส่งแบบมาตรฐาน,J&T Express,,l***_6070,ร****** ม************,(+66)090*****95,***** ,ไทย,กระบี่,เกาะลันตา,,*******************************************,**************,PromptPay,0.1,เครื่องหอมในบ้าน,1165223003747878878 ,,Unchecked,,,,,,,,,
|
||||
579492024561207087 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112969076 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,200,200,0,0,200,38,38,0,0,,0,238,,04/07/2025 08:40:04 ,06/07/2025 15:37:10 ,04/07/2025 09:30:06 ,04/07/2025 11:45:40 ,06/07/2025 15:37:10 ," ",,,Fulfillment by seller,คลังหลัก,761368266606,การจัดส่งแบบมาตรฐาน,J&T Express,,c***tanaleklersbamrung,ฉ***** เ************,(+66)091*****20,***** ,ไทย,ชัยภูมิ,หนองบัวแดง,,***********************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1165162257578297135 ,,Unchecked,,,,,,,,,
|
||||
579487117682312470 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,200,400,0,0,400,30,35,0,5,,0,430,,03/07/2025 19:42:30 ,05/07/2025 11:08:23 ,04/07/2025 06:49:52 ,04/07/2025 11:45:40 ,05/07/2025 11:08:23 ," ",,,Fulfillment by seller,คลังหลัก,761341762436,การจัดส่งแบบมาตรฐาน,J&T Express,,f***tmmobile,ว***** พ*********,(+66)080*****21,***** ,ไทย,สมุทรปราการ,พระสมุทรเจดีย์,,***************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1165138129605395734 ,,Unchecked,,,,,,,,,
|
||||
579374290317575635 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,45,45,0,0,,0,195,,24/06/2025 23:02:25 ,27/06/2025 13:16:22 ,25/06/2025 06:13:59 ,25/06/2025 12:07:42 ,27/06/2025 13:16:22 ," ",,,Fulfillment by seller,คลังหลัก,761067967516,การจัดส่งแบบมาตรฐาน,J&T Express,,0***31nan,ก****** ร*******,(+66)063*****19,***** ,ไทย,นครพนม,บ้านแพง,,****************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1164517828953867731 ,,Unchecked,,,,,,,,,
|
||||
579303155394971154 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732269615675640180 ,P-SKU-OZO-CANTALOUPE,น้ำหอมระเหย ขวดเล็กพกพาสะดวก กลิ่น แคนตาลูป ปรับอารมณ์ให้ดีขึ้น,120 ml,1,0,185,185,0,0,185,45,45,0,0,,0,230,,18/06/2025 20:47:55 ,18/06/2025 20:47:57 ,19/06/2025 11:29:48 ,20/06/2025 11:54:33 ,21/06/2025 20:40:43 ," ",,,Fulfillment by seller,คลังหลัก,760941856032,การจัดส่งแบบมาตรฐาน,J&T Express,,u***2610738375716,ว****** เ**********,(+66)803****91,***** ,ไทย,สิงห์บุรี,บางระจัน,,****************************,,TrueMoney,0.12,เครื่องหอมในบ้าน,1164137018744866322 ,,Unchecked,,,,,,,,,
|
||||
579285880106223153 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333333637859700 ,P-SKU-OZO-OCEAN,น้ำมันหอมระเหย สมุนไพรอโรม่า กลิ่น มหาสมุทร ขนาดมาตรฐาน ใช้ได้นาน ใช้งานง่าย ขวดหยด,30 ml,1,0,79,79,0,0,79,45,45,0,0,,0,203,,17/06/2025 11:41:21 ,19/06/2025 10:52:57 ,17/06/2025 18:18:04 ,18/06/2025 11:59:10 ,19/06/2025 10:52:57 ," ",,,Fulfillment by seller,คลังหลัก,760830503650,การจัดส่งแบบมาตรฐาน,J&T Express,,j***unch,k**********,(+66)081*****42,***** ,ไทย,สระบุรี,แก่งคอย,,*******************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1164038912169969201 ,,Unchecked,,,,,,,,,
|
||||
579285880106223153 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256169710964 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น ลาเวนเดอร์,1,0,79,79,0,0,79,45,45,0,0,,0,203,,17/06/2025 11:41:21 ,19/06/2025 10:52:57 ,17/06/2025 18:18:04 ,18/06/2025 11:59:10 ,19/06/2025 10:52:57 ," ",,,Fulfillment by seller,คลังหลัก,760830503650,การจัดส่งแบบมาตรฐาน,J&T Express,,j***unch,k**********,(+66)081*****42,***** ,ไทย,สระบุรี,แก่งคอย,,*******************************************************************,,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1164038912169969201 ,,Unchecked,,,,,,,,,
|
||||
579284135518242778 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134435908520308 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,2,0,180,360,0,0,360,45,45,0,0,,0,405,,17/06/2025 09:16:29 ,19/06/2025 16:39:31 ,17/06/2025 18:16:21 ,18/06/2025 11:59:11 ,19/06/2025 16:39:31 ," ",,,Fulfillment by seller,คลังหลัก,760809501642,การจัดส่งแบบมาตรฐาน,J&T Express,,s***o9925,อ******** แ****,(+66)917****41,***** ,ไทย,ฉะเชิงเทรา,แปลงยาว,,***********************************************************,,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1164028774671026138 ,,Unchecked,,,,,,,,,
|
||||
579283263498978806 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112903540 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,200,200,0,0,200,45,45,0,0,,0,245,,17/06/2025 07:28:03 ,19/06/2025 18:31:53 ,17/06/2025 08:17:39 ,17/06/2025 08:31:56 ,19/06/2025 18:31:53 ," ",,,Fulfillment by seller,คลังหลัก,760839027410,การจัดส่งแบบมาตรฐาน,J&T Express,,p***acats..123,ณ***** น***********,(+66)895****26,***** ,ไทย,สุราษฎร์ธานี,เกาะสมุย,,***********,*********,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1164021178899400182 ,,Unchecked,,,,,,,,,
|
||||
579279765784397693 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169182425352564 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,แคนตาลูป,1,0,200,200,0,0,200,45,45,0,0,,0,245,,16/06/2025 20:34:58 ,18/06/2025 14:28:10 ,17/06/2025 06:35:00 ,17/06/2025 08:31:56 ,18/06/2025 14:28:10 ," ",,,Fulfillment by seller,คลังหลัก,760846582322,การจัดส่งแบบมาตรฐาน,J&T Express,,f***.beer157,Y***,(+66)863****67,***** ,ไทย,จันทบุรี,เมืองจันทบุรี,,*********************************************,****************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1164005951456774013 ,,Unchecked,,,,,,,,,
|
||||
579258360285594897 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112969076 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,200,200,160,0,40,0,45,0,45,,0,40,,14/06/2025 23:04:13 ,14/06/2025 23:06:00 ,15/06/2025 17:43:27 ,16/06/2025 08:06:12 ,18/06/2025 12:01:41 ," ",,,Fulfillment by seller,คลังหลัก,760736285982,การจัดส่งแบบมาตรฐาน,J&T Express,,,ป****,(+66)065*****38,***** ,ไทย,ลพบุรี,เมืองลพบุรี,,**********,*****************************************************************************,PromptPay,0.1,เครื่องหอมในบ้าน,1163885139879167249 ,,Unchecked,,,,,,,,,
|
||||
579223985199089302 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,29,29,0,0,,0,229,,12/06/2025 09:05:28 ,14/06/2025 15:37:04 ,12/06/2025 09:49:02 ,13/06/2025 09:40:33 ,14/06/2025 15:37:04 ," ",,,Fulfillment by seller,คลังหลัก,760729996106,การจัดส่งแบบมาตรฐาน,J&T Express,,n***5563,ช*** น***,(+66)062*****96,***** ,ไทย,ราชบุรี,โพธาราม,,**************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163705218341439126 ,,Unchecked,,,,,,,,,
|
||||
579217824805127304 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,45,45,0,0,,0,195,,11/06/2025 19:19:51 ,11/06/2025 19:20:18 ,12/06/2025 06:10:57 ,12/06/2025 07:54:16 ,13/06/2025 11:34:24 ," ",,,Fulfillment by seller,คลังหลัก,760607979972,การจัดส่งแบบมาตรฐาน,J&T Express,,m***mg122,P*****,(+66)804****30,***** ,Thailand,Bangkok,Vadhana,,*********************************************,*********,Mbanking,0.1,เครื่องหอมในบ้าน,1163678801730372744 ,,Unchecked,,,,,,,,,
|
||||
579164886793159860 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732134087248873844 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ยูคาลิปตัส สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,60 ml,1,1,130,130,0,0,130,45,45,0,0,,0,175,130,07/06/2025 21:57:00 ," ",08/06/2025 08:04:37 ,09/06/2025 08:37:57 ," ",16/06/2025 12:22:48 ,System,การจัดส่งพัสดุไม่สำเร็จ,Fulfillment by seller,คลังหลัก,760505144814,การจัดส่งแบบมาตรฐาน,J&T Express,,s***cha.panpanich,s****************,(+66)096*****71,***** ,ไทย,สมุทรปราการ,บางพลี,,******************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163425743006106804 ,,Unchecked,,,,,,,,,
|
||||
579151090898143047 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134145388546420 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น โอเชี่ยน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,29,29,0,0,,0,187,,06/06/2025 21:18:32 ,09/06/2025 11:13:25 ,07/06/2025 05:35:21 ,07/06/2025 11:27:30 ,09/06/2025 11:13:25 ," ",,,Fulfillment by seller,คลังหลัก,760543213603,การจัดส่งแบบมาตรฐาน,J&T Express,,p***mongkol,ไ**** ส*****,(+66)097*****59,***** ,ไทย,เพชรบุรี,เขาย้อย,,*******************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163355174703433543 ,,Unchecked,,,,,,,,,
|
||||
579151090898143047 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134506631825780 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น โอโซน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,29,29,0,0,,0,187,,06/06/2025 21:18:32 ,09/06/2025 11:13:25 ,07/06/2025 05:35:21 ,07/06/2025 11:27:30 ,09/06/2025 11:13:25 ," ",,,Fulfillment by seller,คลังหลัก,760543213603,การจัดส่งแบบมาตรฐาน,J&T Express,,p***mongkol,ไ**** ส*****,(+66)097*****59,***** ,ไทย,เพชรบุรี,เขาย้อย,,*******************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163355174703433543 ,,Unchecked,,,,,,,,,
|
||||
579151067863746375 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732134145388743028 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น โอเชี่ยน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,1,180,180,0,0,180,29,29,0,0,,0,209,180,06/06/2025 21:16:52 ," "," "," "," ",06/06/2025 21:18:09 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,p***mongkol,ไ**** ส*****,(+66)097*****59,***** ,ไทย,เพชรบุรี,เขาย้อย,,*******************************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163354949767235399 ,,Unchecked,,,,,,,,,
|
||||
579100927341266758 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,1,79,79,0,0,79,45,45,0,0,,0,124,79,03/06/2025 13:01:43 ," "," "," "," ",03/06/2025 13:18:49 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,u***854980280181,ว******** ส******,(+66)831****42,***** ,ไทย,นนทบุรี,บางบัวทอง,,************************,************************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163140634649134918 ,,Unchecked,,,,,,,,,
|
||||
579100536602265204 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623513972 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,200,200,12,0,188,29,29,0,0,,0,217,,03/06/2025 12:35:37 ,09/06/2025 14:25:17 ,03/06/2025 19:08:19 ,04/06/2025 08:56:01 ,09/06/2025 14:25:17 ," ",,,Fulfillment by seller,คลังหลัก,760456641453,การจัดส่งแบบมาตรฐาน,J&T Express,,s***pornsupaporn34,ส******* ถ****,(+66)086*****32,***** ,ราชอาณาจักรไทย,ตาก,เมืองตาก,,*********************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163139102927783540 ,,Unchecked,,,,,,,,,
|
||||
579091183912912150 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,02/06/2025 18:47:35 ,04/06/2025 17:23:31 ,02/06/2025 18:49:24 ,03/06/2025 08:23:07 ,04/06/2025 17:23:31 ," ",,,Fulfillment by seller,คลังหลัก,760443127146,การจัดส่งแบบมาตรฐาน,J&T Express,,f***tmmobile,ว***** พ*********,(+66)080*****21,***** ,ไทย,สมุทรปราการ,พระสมุทรเจดีย์,,***************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1163099845251400982 ,,Unchecked,,,,,,,,,
|
||||
579074645977761504 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134356249315700 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น มะลิ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,85 ml,1,0,150,150,0,0,150,45,45,0,0,,0,195,,01/06/2025 15:04:26 ,01/06/2025 15:05:36 ,01/06/2025 17:25:39 ,02/06/2025 11:37:28 ,04/06/2025 13:48:31 ," ",,,Fulfillment by seller,คลังหลัก,760374575682,การจัดส่งแบบมาตรฐาน,J&T Express,,s***awkhamkham772,อ******,(+66)094*****38,***** ,ไทย,เชียงใหม่,เมืองเชียงใหม่,,*************************************************************************************************,,PromptPay,0.1,เครื่องหอมในบ้าน,1163025825490175712 ,,Unchecked,,,,,,,,,
|
||||
579050517960426801 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435272564 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,150,150,0,0,150,29,29,0,0,,0,179,,30/05/2025 17:11:44 ,01/06/2025 13:29:46 ,31/05/2025 06:59:34 ,31/05/2025 08:31:14 ,01/06/2025 13:29:46 ," ",,,Fulfillment by seller,คลังหลัก,760392669153,การจัดส่งแบบมาตรฐาน,J&T Express,,n***abk2,ณ******* บ*******,(+66)098*****35,***** ,ไทย,ประจวบคีรีขันธ์,ปราณบุรี,,******************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162926764465816881 ,,Unchecked,,,,,,,,,
|
||||
579028838833686526 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,40,45,0,5,,0,340,,29/05/2025 01:12:37 ,29/05/2025 01:12:59 ,29/05/2025 05:57:49 ,29/05/2025 08:16:31 ,31/05/2025 20:15:31 ," ",,,Fulfillment by seller,คลังหลัก,760215834582,การจัดส่งแบบมาตรฐาน,J&T Express,,k***p,เ****,(+66)061*****85,***** ,ไทย,อุบลราชธานี,เดชอุดม,,***********************************,*******,Mbanking,0.1,เครื่องหอมในบ้าน,1162826115100674046 ,,Unchecked,,,,,,,,,
|
||||
579028838833686526 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435207028 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,มะลิ,1,0,150,150,0,0,150,40,45,0,5,,0,340,,29/05/2025 01:12:37 ,29/05/2025 01:12:59 ,29/05/2025 05:57:49 ,29/05/2025 08:16:31 ,31/05/2025 20:15:31 ," ",,,Fulfillment by seller,คลังหลัก,760215834582,การจัดส่งแบบมาตรฐาน,J&T Express,,k***p,เ****,(+66)061*****85,***** ,ไทย,อุบลราชธานี,เดชอุดม,,***********************************,*******,Mbanking,0.1,เครื่องหอมในบ้าน,1162826115100674046 ,,Unchecked,,,,,,,,,
|
||||
578988215637346266 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,45,45,0,0,,0,645,,26/05/2025 00:19:48 ,27/05/2025 20:37:46 ,26/05/2025 06:01:25 ,26/05/2025 15:54:50 ,27/05/2025 20:37:46 ," ",,,Fulfillment by seller,คลังหลัก,760161592536,การจัดส่งแบบมาตรฐาน,J&T Express,,s***o9925,อ******** แ****,(+66)917****41,***** ,ไทย,ฉะเชิงเทรา,แปลงยาว,,***********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162644952893786074 ,,Unchecked,,,,,,,,,
|
||||
578988215637346266 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435141492 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,150,150,0,0,150,45,45,0,0,,0,645,,26/05/2025 00:19:48 ,27/05/2025 20:37:46 ,26/05/2025 06:01:25 ,26/05/2025 15:54:50 ,27/05/2025 20:37:46 ," ",,,Fulfillment by seller,คลังหลัก,760161592536,การจัดส่งแบบมาตรฐาน,J&T Express,,s***o9925,อ******** แ****,(+66)917****41,***** ,ไทย,ฉะเชิงเทรา,แปลงยาว,,***********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162644952893786074 ,,Unchecked,,,,,,,,,
|
||||
578988215637346266 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435469172 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,โอเชี่ยน,1,0,150,150,0,0,150,45,45,0,0,,0,645,,26/05/2025 00:19:48 ,27/05/2025 20:37:46 ,26/05/2025 06:01:25 ,26/05/2025 15:54:50 ,27/05/2025 20:37:46 ," ",,,Fulfillment by seller,คลังหลัก,760161592536,การจัดส่งแบบมาตรฐาน,J&T Express,,s***o9925,อ******** แ****,(+66)917****41,***** ,ไทย,ฉะเชิงเทรา,แปลงยาว,,***********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162644952893786074 ,,Unchecked,,,,,,,,,
|
||||
578988215637346266 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,150,150,0,0,150,45,45,0,0,,0,645,,26/05/2025 00:19:48 ,27/05/2025 20:37:46 ,26/05/2025 06:01:25 ,26/05/2025 15:54:50 ,27/05/2025 20:37:46 ," ",,,Fulfillment by seller,คลังหลัก,760161592536,การจัดส่งแบบมาตรฐาน,J&T Express,,s***o9925,อ******** แ****,(+66)917****41,***** ,ไทย,ฉะเชิงเทรา,แปลงยาว,,***********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162644952893786074 ,,Unchecked,,,,,,,,,
|
||||
578983107276605359 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435010420 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,150,150,0,0,150,45,45,0,0,,0,195,,25/05/2025 16:06:02 ,25/05/2025 16:06:17 ,26/05/2025 05:59:33 ,26/05/2025 15:54:50 ,27/05/2025 15:13:00 ," ",,,Fulfillment by seller,คลังหลัก,760179797521,การจัดส่งแบบมาตรฐาน,J&T Express,,a***ontours1,S*** Q**** A**** S***,(+66)971****00,***** ,Thailand,Chon Buri,Bang Lamung,,***********************************************,*****************************,Mbanking,0.1,เครื่องหอมในบ้าน,1162627606188689327 ,,Unchecked,,,,,,,,,
|
||||
578977515051779707 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,25/05/2025 07:02:31 ,27/05/2025 14:34:14 ,25/05/2025 08:02:35 ,26/05/2025 15:54:51 ,27/05/2025 14:34:14 ," ",,,Fulfillment by seller,คลังหลัก,760176362326,การจัดส่งแบบมาตรฐาน,J&T Express,,j***5664,ก***** บ********,(+66)065*****59,***** ,ไทย,ระยอง,บ้านฉาง,,********************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162601538614036091 ,,Unchecked,,,,,,,,,
|
||||
578951890452776113 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,23/05/2025 07:29:56 ,23/05/2025 07:29:58 ,23/05/2025 07:51:32 ,23/05/2025 08:36:24 ,27/05/2025 19:21:44 ," ",,,Fulfillment by seller,คลังหลัก,760024285801,การจัดส่งแบบมาตรฐาน,J&T Express,,o***52511,ส**** ป********* น*******,(+66)878****62,***** ,ไทย,อุบลราชธานี,เมืองอุบลราชธานี,,*******************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1162493181025223857 ,,Unchecked,,,,,,,,,
|
||||
578951890452776113 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,23/05/2025 07:29:56 ,23/05/2025 07:29:58 ,23/05/2025 07:51:32 ,23/05/2025 08:36:24 ,27/05/2025 19:21:44 ," ",,,Fulfillment by seller,คลังหลัก,760024285801,การจัดส่งแบบมาตรฐาน,J&T Express,,o***52511,ส**** ป********* น*******,(+66)878****62,***** ,ไทย,อุบลราชธานี,เมืองอุบลราชธานี,,*******************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1162493181025223857 ,,Unchecked,,,,,,,,,
|
||||
578944088262673745 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623513972 ,120-Pro-Combo,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,200,200,0,0,200,45,45,0,0,,0,245,,22/05/2025 15:33:56 ,25/05/2025 13:50:06 ,22/05/2025 17:45:26 ,23/05/2025 08:36:24 ,25/05/2025 13:50:06 ," ",,,Fulfillment by seller,คลังหลัก,760033806755,การจัดส่งแบบมาตรฐาน,J&T Express,,n***ky175,ป******** พ***********,(+66)982****42,***** ,ไทย,เชียงใหม่,เมืองเชียงใหม่,,******************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162465923008922961 ,,Unchecked,,,,,,,,,
|
||||
578915685252499251 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732133222530517364 ,,Hom Aroma น้ำมันหอมระเหย กลิ่นตะไคร้บ้าน สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,2,0,79,158,0,0,158,45,45,0,0,,0,203,,20/05/2025 17:33:10 ,20/05/2025 17:33:14 ,21/05/2025 06:36:39 ,21/05/2025 13:02:32 ,22/05/2025 19:25:49 ," ",,,Fulfillment by seller,คลังหลัก,760051463146,การจัดส่งแบบมาตรฐาน,J&T Express,,u***06679821,ม*******,(+66)085*****91,***** ,Thailand,Nonthaburi,Bang Kruai,,***************************************************,*******************************************************************************************************,บัตรเครดิต/เดบิต,0.2,เครื่องหอมในบ้าน,1162359873401489203 ,,Unchecked,,,,,,,,,
|
||||
578912331776427827 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,1,200,200,0,0,200,45,45,0,0,,0,245,200,20/05/2025 13:53:25 ,20/05/2025 13:53:28 ," "," "," ",20/05/2025 13:55:48 ,User,มีราคาที่ดีกว่า,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,u***06679821,ม*******,(+66)085*****91,***** ,Thailand,Nonthaburi,Bang Kruai,,***************************************************,*******************************************************************************************************,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1162347321198217011 ,,Unchecked,,,,,,,,,
|
||||
578912331776427827 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,1,0,0,0,0,0,45,45,0,0,,0,245,0,20/05/2025 13:53:25 ,20/05/2025 13:53:28 ," "," "," ",20/05/2025 13:55:48 ,User,มีราคาที่ดีกว่า,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,u***06679821,ม*******,(+66)085*****91,***** ,Thailand,Nonthaburi,Bang Kruai,,***************************************************,*******************************************************************************************************,บัตรเครดิต/เดบิต,0.1,เครื่องหอมในบ้าน,1162347321198217011 ,,Unchecked,,,,,,,,,
|
||||
578886722529428518 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465434944884 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,0,150,300,0,15,285,95,95,0,0,,0,380,,18/05/2025 14:21:48 ,21/05/2025 20:47:11 ,18/05/2025 17:34:44 ,19/05/2025 11:39:02 ,21/05/2025 20:47:11 ," ",,,Fulfillment by seller,คลังหลัก,758920515835,การจัดส่งแบบมาตรฐาน,J&T Express,,m***natkritta,P******** C****** L*******,(+66)088*****53,***** ,ไทย,สุราษฎร์ธานี,เกาะพะงัน,,*************************,***************************,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1162235665988486182 ,,Unchecked,,,,,,,,,
|
||||
578886719107335206 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732169146112641396 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,2,2,200,400,25,15,360,95,95,0,0,,0,455,360,18/05/2025 14:20:54 ," "," "," "," ",18/05/2025 14:22:17 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,m***natkritta,P******** C****** L*******,(+66)088*****53,***** ,ไทย,สุราษฎร์ธานี,เกาะพะงัน,,*************************,***************************,การเก็บเงินปลายทาง,0.2,เครื่องหอมในบ้าน,1162235280231072806 ,,Unchecked,,,,,,,,,
|
||||
578886719107335206 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,1,0,0,0,0,0,95,95,0,0,,0,455,0,18/05/2025 14:20:54 ," "," "," "," ",18/05/2025 14:22:17 ,User,ไม่ต้องการแล้ว,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,m***natkritta,P******** C****** L*******,(+66)088*****53,***** ,ไทย,สุราษฎร์ธานี,เกาะพะงัน,,*************************,***************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162235280231072806 ,,Unchecked,,,,,,,,,
|
||||
578886258799839200 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112969076 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,200,200,0,0,200,45,45,0,0,,0,245,,18/05/2025 13:37:16 ,18/05/2025 13:39:27 ,18/05/2025 17:34:43 ,19/05/2025 11:39:02 ,20/05/2025 18:09:17 ," ",,,Fulfillment by seller,คลังหลัก,758920444903,การจัดส่งแบบมาตรฐาน,J&T Express,,s***so4,พ**** บ*********,(+66)941****41,***** ,ไทย,สมุทรปราการ,บางเสาธง,,**********************************************,***********,Mbanking,0.1,เครื่องหอมในบ้าน,1162233274797230048 ,,Unchecked,,,,,,,,,
|
||||
578886258799839200 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,18/05/2025 13:37:16 ,18/05/2025 13:39:27 ,18/05/2025 17:34:43 ,19/05/2025 11:39:02 ,20/05/2025 18:09:17 ," ",,,Fulfillment by seller,คลังหลัก,758920444903,การจัดส่งแบบมาตรฐาน,J&T Express,,s***so4,พ**** บ*********,(+66)941****41,***** ,ไทย,สมุทรปราการ,บางเสาธง,,**********************************************,***********,Mbanking,0.1,เครื่องหอมในบ้าน,1162233274797230048 ,,Unchecked,,,,,,,,,
|
||||
578882864896574627 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112838004 ,120-FREE-30,[ฟรีขวดเล็ก 30 ml] Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,200,200,0,0,200,45,45,0,0,,0,245,,18/05/2025 08:21:33 ,18/05/2025 08:21:54 ,18/05/2025 17:34:44 ,19/05/2025 11:39:02 ,20/05/2025 19:51:36 ," ",,,Fulfillment by seller,คลังหลัก,758926244904,การจัดส่งแบบมาตรฐาน,J&T Express,,u***121462096,อ**** ว****,(+66)089*****32,***** ,ไทย,กรุงเทพมหานคร,พญาไท,,*******************************************************************,,Mbanking,0.1,เครื่องหอมในบ้าน,1162218011193214115 ,,Unchecked,,,,,,,,,
|
||||
578882864896574627 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,18/05/2025 08:21:33 ,18/05/2025 08:21:54 ,18/05/2025 17:34:44 ,19/05/2025 11:39:02 ,20/05/2025 19:51:36 ," ",,,Fulfillment by seller,คลังหลัก,758926244904,การจัดส่งแบบมาตรฐาน,J&T Express,,u***121462096,อ**** ว****,(+66)089*****32,***** ,ไทย,กรุงเทพมหานคร,พญาไท,,*******************************************************************,,Mbanking,0.1,เครื่องหอมในบ้าน,1162218011193214115 ,,Unchecked,,,,,,,,,
|
||||
578854353645045238 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465435075956 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,วานิลลา,1,0,150,150,8.33,5,136.67,45,45,0,0,,0,455,,15/05/2025 23:46:45 ,17/05/2025 16:47:18 ,16/05/2025 06:14:43 ,16/05/2025 14:18:01 ,17/05/2025 16:47:18 ," ",,,Fulfillment by seller,คลังหลัก,758850501614,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ntha.kornkoseeyakaj,ส***** ก*********,(+66)865****33,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตสะพานสูง,,*******************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162099227653801462 ,,Unchecked,,,,,,,,,
|
||||
578854353645045238 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406465436321140 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,150,150,8.34,5,136.66,45,45,0,0,,0,455,,15/05/2025 23:46:45 ,17/05/2025 16:47:18 ,16/05/2025 06:14:43 ,16/05/2025 14:18:01 ,17/05/2025 16:47:18 ," ",,,Fulfillment by seller,คลังหลัก,758850501614,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ntha.kornkoseeyakaj,ส***** ก*********,(+66)865****33,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตสะพานสูง,,*******************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162099227653801462 ,,Unchecked,,,,,,,,,
|
||||
578854353645045238 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731406473357460852 ,NMP,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ไทยสปา,1,0,150,150,8.33,5,136.67,45,45,0,0,,0,455,,15/05/2025 23:46:45 ,17/05/2025 16:47:18 ,16/05/2025 06:14:43 ,16/05/2025 14:18:01 ,17/05/2025 16:47:18 ," ",,,Fulfillment by seller,คลังหลัก,758850501614,การจัดส่งแบบมาตรฐาน,J&T Express,,s***ntha.kornkoseeyakaj,ส***** ก*********,(+66)865****33,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตสะพานสูง,,*******************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1162099227653801462 ,,Unchecked,,,,,,,,,
|
||||
578826654510188044 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215020771149172 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น ตะไคร้,1,0,79,79,0,0,79,45,45,0,0,,0,124,,13/05/2025 20:38:20 ,15/05/2025 12:28:27 ,14/05/2025 07:20:22 ,14/05/2025 14:16:35 ,15/05/2025 12:28:27 ," ",,,Fulfillment by seller,คลังหลัก,758790633882,การจัดส่งแบบมาตรฐาน,J&T Express,,a***hanyawee,A************,(+66)872****88,***** ,ไทย,กรุงเทพฯ,พญาไท,,*****************,***********************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1161976242266998284 ,,Unchecked,,,,,,,,,
|
||||
578761785681283018 ,ยกเลิกแล้ว,ยกเลิกแล้ว,Cancel,Normal,1732134435908454772 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น กุหลาบแดง สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,85 ml,1,1,150,150,0,0,150,45,45,0,0,,0,195,150,08/05/2025 16:31:45 ," "," "," "," ",09/05/2025 08:58:40 ,User,จำเป็นต้องเปลี่ยนที่อยู่จัดส่ง,Fulfillment by seller,คลังหลัก,,การจัดส่งแบบมาตรฐาน,,,n***m872,น***********,(+66)064*****25,***** ,ไทย,สมุทรปราการ,บางพลี,,********************,**********************************************************,PromptPay,0.1,เครื่องหอมในบ้าน," ",,,,,,,,,,,
|
||||
578756236230887004 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134484429474164 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ผลไม้รวม สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,120 ml,1,0,180,180,0,0,180,45,45,0,0,,0,225,,08/05/2025 10:22:01 ,10/05/2025 13:26:42 ,08/05/2025 11:53:44 ,08/05/2025 12:23:04 ,10/05/2025 13:26:42 ," ",,,Fulfillment by seller,คลังหลัก,758631301050,การจัดส่งแบบมาตรฐาน,J&T Express,,k***thin.k_win,ก*** ไ*****,(+66)089*****10,***** ,ไทย,เชียงใหม่,สันป่าตอง,,**********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161661044415759964 ,,Unchecked,,,,,,,,,
|
||||
578741378930869737 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134529418234228 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกปีบ สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,45,45,0,0,,0,124,,07/05/2025 09:56:07 ,10/05/2025 19:32:15 ,07/05/2025 10:18:28 ,08/05/2025 07:58:01 ,10/05/2025 19:32:15 ," ",,,Fulfillment by seller,คลังหลัก,758599573826,การจัดส่งแบบมาตรฐาน,J&T Express,,m***ipsrisawad,m*************,(+66)083*****38,***** ,ไทย,สงขลา,เทพา,,****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161596544745899497 ,,Unchecked,,,,,,,,,
|
||||
578737781738865798 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623513972 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml โปรยิงซื้อยิ่งลด,ลาเวนเดอร์,1,0,200,200,0,17.5,182.5,45,45,0,0,,0,410,,06/05/2025 22:22:26 ,06/05/2025 22:23:26 ,07/05/2025 07:32:33 ,07/05/2025 14:59:05 ,09/05/2025 14:07:59 ," ",,,Fulfillment by seller,คลังหลัก,758587835725,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1474995459245,อ*** ท******,(+66)818****77,***** ,ไทย,กระบี่,เมืองกระบี่,,*************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161577506373534854 ,,Unchecked,,,,,,,,,
|
||||
578737781738865798 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484625348980 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml โปรยิงซื้อยิ่งลด,ไทยสปา,1,0,200,200,0,17.5,182.5,45,45,0,0,,0,410,,06/05/2025 22:22:26 ,06/05/2025 22:23:26 ,07/05/2025 07:32:33 ,07/05/2025 14:59:05 ,09/05/2025 14:07:59 ," ",,,Fulfillment by seller,คลังหลัก,758587835725,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1474995459245,อ*** ท******,(+66)818****77,***** ,ไทย,กระบี่,เมืองกระบี่,,*************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161577506373534854 ,,Unchecked,,,,,,,,,
|
||||
578728368875406473 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623513972 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml โปรยิงซื้อยิ่งลด,ลาเวนเดอร์,1,0,200,200,9.61,0,190.39,45,45,0,0,,0,235.39,,06/05/2025 11:12:42 ,06/05/2025 11:12:44 ,06/05/2025 11:15:03 ,06/05/2025 14:01:27 ,08/05/2025 11:37:27 ," ",,,Fulfillment by seller,คลังหลัก,758561178966,การจัดส่งแบบมาตรฐาน,J&T Express,,p***la_aa,ส****** ป*******,(+66)982****32,***** ,ไทย,หนองคาย,เมืองหนองคาย,,*************************************************************************************************************************,*************************************************************************************************************************,PayLater,0.1,เครื่องหอมในบ้าน,1161541392301917321 ,,Unchecked,,,,,,,,,
|
||||
578721144605738244 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,05/05/2025 18:48:04 ,05/05/2025 18:49:16 ,06/05/2025 08:52:43 ,06/05/2025 14:01:24 ,08/05/2025 14:37:59 ," ",,,Fulfillment by seller,คลังหลัก,758564900236,การจัดส่งแบบมาตรฐาน,J&T Express,,k***y.ahn1,K** A**,(+66)080*****81,***** ,Thailand,Krabi,Mueang Krabi,,*****************************************************************,********,PromptPay,0.1,เครื่องหอมในบ้าน,1161506477713818884 ,,Unchecked,,,,,,,,,
|
||||
578721144605738244 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112706932 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ลาเวนเดอร์,1,0,200,200,0,0,200,45,45,0,0,,0,245,,05/05/2025 18:48:04 ,05/05/2025 18:49:16 ,06/05/2025 08:52:43 ,06/05/2025 14:01:24 ,08/05/2025 14:37:59 ," ",,,Fulfillment by seller,คลังหลัก,758564900236,การจัดส่งแบบมาตรฐาน,J&T Express,,k***y.ahn1,K** A**,(+66)080*****81,***** ,Thailand,Krabi,Mueang Krabi,,*****************************************************************,********,PromptPay,0.1,เครื่องหอมในบ้าน,1161506477713818884 ,,Unchecked,,,,,,,,,
|
||||
578707061626995783 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,40,45,0,5,,0,575,,04/05/2025 17:45:41 ,04/05/2025 17:45:43 ,04/05/2025 18:33:03 ,05/05/2025 13:45:01 ,06/05/2025 17:46:46 ," ",,,Fulfillment by seller,คลังหลัก,758500554405,การจัดส่งแบบมาตรฐาน,J&T Express,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161437857280656455 ,,Unchecked,,,,,,,,,
|
||||
578707061626995783 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112969076 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ยูคาลิปตัส,1,0,200,200,16.67,5,178.33,40,45,0,5,,0,575,,04/05/2025 17:45:41 ,04/05/2025 17:45:43 ,04/05/2025 18:33:03 ,05/05/2025 13:45:01 ,06/05/2025 17:46:46 ," ",,,Fulfillment by seller,คลังหลัก,758500554405,การจัดส่งแบบมาตรฐาน,J&T Express,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161437857280656455 ,,Unchecked,,,,,,,,,
|
||||
578707061626995783 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146113034612 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เฟรช ลาเวนเดอร์,1,0,200,200,16.66,5,178.34,40,45,0,5,,0,575,,04/05/2025 17:45:41 ,04/05/2025 17:45:43 ,04/05/2025 18:33:03 ,05/05/2025 13:45:01 ,06/05/2025 17:46:46 ," ",,,Fulfillment by seller,คลังหลัก,758500554405,การจัดส่งแบบมาตรฐาน,J&T Express,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161437857280656455 ,,Unchecked,,,,,,,,,
|
||||
578707061626995783 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146113100148 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,เปปเปอร์มินต์,1,0,200,200,16.67,5,178.33,40,45,0,5,,0,575,,04/05/2025 17:45:41 ,04/05/2025 17:45:43 ,04/05/2025 18:33:03 ,05/05/2025 13:45:01 ,06/05/2025 17:46:46 ," ",,,Fulfillment by seller,คลังหลัก,758500554405,การจัดส่งแบบมาตรฐาน,J&T Express,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161437857280656455 ,,Unchecked,,,,,,,,,
|
||||
578706985596519495 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169484623776116 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml โปรยิงซื้อยิ่งลด,ยูคาลิปตัส,1,0,200,200,0,0,200,0,45,0,45,,0,200,,04/05/2025 17:38:36 ,04/05/2025 17:38:38 ,04/05/2025 18:33:03 ,05/05/2025 13:45:01 ,06/05/2025 17:46:46 ," ",,,Fulfillment by seller,คลังหลัก,758500554405,การจัดส่งแบบมาตรฐาน,J&T Express,,p***09,ศ******** ต***********,(+66)661****64,***** ,ราชอาณาจักรไทย,กรุงเทพมหานคร,เขตลาดพร้าว,,**************************************************************************,,TrueMoney,0.1,เครื่องหอมในบ้าน,1161437857280656455 ,,Unchecked,,,,,,,,,
|
||||
578671529037497761 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169504619137396 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml โปรยิงซื้อยิ่งลด,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,01/05/2025 18:24:11 ,04/05/2025 18:05:02 ,01/05/2025 19:32:45 ,02/05/2025 12:59:44 ,04/05/2025 18:05:02 ," ",,,Fulfillment by seller,คลังหลัก,758404897666,การจัดส่งแบบมาตรฐาน,J&T Express,,l***wan.raksa,L****** R****,(+66)062*****97,***** ,ไทย,บุรีรัมย์,ประโคนชัย,,*****************************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161269507212674465 ,,Unchecked,,,,,,,,,
|
||||
578671056539911233 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,29,29,0,0,,0,229,,01/05/2025 17:39:01 ,04/05/2025 16:53:28 ,01/05/2025 18:19:38 ,02/05/2025 12:59:44 ,04/05/2025 16:53:28 ," ",,,Fulfillment by seller,คลังหลัก,758404989554,การจัดส่งแบบมาตรฐาน,J&T Express,,n***chanewwww,ณ******,(+66)092*****47,***** ,ไทย,ประจวบคีรีขันธ์,บางสะพาน,,****************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161267684317627457 ,,Unchecked,,,,,,,,,
|
||||
578671056539911233 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,29,29,0,0,,0,229,,01/05/2025 17:39:01 ,04/05/2025 16:53:28 ,01/05/2025 18:19:38 ,02/05/2025 12:59:44 ,04/05/2025 16:53:28 ," ",,,Fulfillment by seller,คลังหลัก,758404989554,การจัดส่งแบบมาตรฐาน,J&T Express,,n***chanewwww,ณ******,(+66)092*****47,***** ,ไทย,ประจวบคีรีขันธ์,บางสะพาน,,****************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161267684317627457 ,,Unchecked,,,,,,,,,
|
||||
578670836016645412 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,01/05/2025 17:14:14 ,03/05/2025 18:15:13 ,01/05/2025 18:19:38 ,02/05/2025 12:59:44 ,03/05/2025 18:15:13 ," ",,,Fulfillment by seller,คลังหลัก,758407589553,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.25167,A*** 2***,(+66)829****70,***** ,ไทย,กรุงเทพฯ,พระโขนง,,********************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161266292792722724 ,,Unchecked,,,,,,,,,
|
||||
578670836016645412 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,01/05/2025 17:14:14 ,03/05/2025 18:15:13 ,01/05/2025 18:19:38 ,02/05/2025 12:59:44 ,03/05/2025 18:15:13 ," ",,,Fulfillment by seller,คลังหลัก,758407589553,การจัดส่งแบบมาตรฐาน,J&T Express,,a***.25167,A*** 2***,(+66)829****70,***** ,ไทย,กรุงเทพฯ,พระโขนง,,********************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161266292792722724 ,,Unchecked,,,,,,,,,
|
||||
578666297315657063 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169182425745780 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,กุหลาบแดง,1,0,200,200,0,7.5,192.5,45,45,0,0,,0,430,,01/05/2025 10:31:48 ,03/05/2025 12:33:02 ,01/05/2025 10:36:51 ,01/05/2025 16:21:41 ,03/05/2025 12:33:02 ," ",,,Fulfillment by seller,คลังหลัก,758393526494,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1998766105986,ณ******* พ*********,(+66)098*****54,***** ,ไทย,น่าน,ท่าวังผา,,*******************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161245364249134439 ,,Unchecked,,,,,,,,,
|
||||
578666297315657063 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,430,,01/05/2025 10:31:48 ,03/05/2025 12:33:02 ,01/05/2025 10:36:51 ,01/05/2025 16:21:41 ,03/05/2025 12:33:02 ," ",,,Fulfillment by seller,คลังหลัก,758393526494,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1998766105986,ณ******* พ*********,(+66)098*****54,***** ,ไทย,น่าน,ท่าวังผา,,*******************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161245364249134439 ,,Unchecked,,,,,,,,,
|
||||
578666297315657063 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112838004 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,200,200,0,7.5,192.5,45,45,0,0,,0,430,,01/05/2025 10:31:48 ,03/05/2025 12:33:02 ,01/05/2025 10:36:51 ,01/05/2025 16:21:41 ,03/05/2025 12:33:02 ," ",,,Fulfillment by seller,คลังหลัก,758393526494,การจัดส่งแบบมาตรฐาน,J&T Express,,u***1998766105986,ณ******* พ*********,(+66)098*****54,***** ,ไทย,น่าน,ท่าวังผา,,*******************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161245364249134439 ,,Unchecked,,,,,,,,,
|
||||
578659198202578056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112641396 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ตะไคร้,1,0,200,200,0,0,200,45,45,0,0,,0,245,,30/04/2025 19:35:54 ,03/05/2025 11:25:37 ,01/05/2025 07:44:28 ,01/05/2025 10:25:26 ,03/05/2025 11:25:37 ," ",,,Fulfillment by seller,คลังหลัก,758384026584,การจัดส่งแบบมาตรฐาน,J&T Express,,a***e_naphaksorn,น***** ป*******,(+66)064*****33,***** ,ไทย,เพชรบูรณ์,ศรีเทพ,,**************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161221653481620616 ,,Unchecked,,,,,,,,,
|
||||
578659198202578056 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,30/04/2025 19:35:54 ,03/05/2025 11:25:37 ,01/05/2025 07:44:28 ,01/05/2025 10:25:26 ,03/05/2025 11:25:37 ," ",,,Fulfillment by seller,คลังหลัก,758384026584,การจัดส่งแบบมาตรฐาน,J&T Express,,a***e_naphaksorn,น***** ป*******,(+66)064*****33,***** ,ไทย,เพชรบูรณ์,ศรีเทพ,,**************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161221653481620616 ,,Unchecked,,,,,,,,,
|
||||
578655823139931936 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169146112838004 ,120-FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดใหญ่ ขนาด 120 ml,ซากุระ,1,0,200,200,0,0,200,45,45,0,0,,0,245,,30/04/2025 14:57:21 ,03/05/2025 14:50:50 ,01/05/2025 07:42:33 ,01/05/2025 10:25:24 ,03/05/2025 14:50:50 ," ",,,Fulfillment by seller,คลังหลัก,758386220583,การจัดส่งแบบมาตรฐาน,J&T Express,,d***z817gtpx,เ*** ส******,(+66)093*****69,***** ,ไทย,นครศรีธรรมราช,ทุ่งสง,,********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161208414899504928 ,,Unchecked,,,,,,,,,
|
||||
578655823139931936 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732169380197468532 ,FREE-30,Hom Aroma น้ำมันหอมระเหย สำหรับเครื่องพ่นอโรม่า ออยโอโซน ขวดเล็ก ขนาด 30 ml รายการของแถม,กำหนดกลิ่นของแถมผ่าน Chat,1,0,0,0,0,0,0,45,45,0,0,,0,245,,30/04/2025 14:57:21 ,03/05/2025 14:50:50 ,01/05/2025 07:42:33 ,01/05/2025 10:25:24 ,03/05/2025 14:50:50 ," ",,,Fulfillment by seller,คลังหลัก,758386220583,การจัดส่งแบบมาตรฐาน,J&T Express,,d***z817gtpx,เ*** ส******,(+66)093*****69,***** ,ไทย,นครศรีธรรมราช,ทุ่งสง,,********************************************************,,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161208414899504928 ,,Unchecked,,,,,,,,,
|
||||
578626692420044200 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732134728508606836 ,,Hom Aroma น้ำมันหอมระเหย กลิ่น ดอกการะเวก สำหรับเครื่องพ่นอโรม่า ออยโอโซน ช่วยเพิ่มกลิ่นหอมในห้อง,30 ml,1,0,79,79,0,0,79,45,45,0,0,,0,203,,28/04/2025 14:38:28 ,30/04/2025 18:39:16 ,29/04/2025 08:24:36 ,29/04/2025 14:30:51 ,30/04/2025 18:39:16 ," ",,,Fulfillment by seller,คลังหลัก,758329805192,การจัดส่งแบบมาตรฐาน,J&T Express,,n***ira246,น******** ศ********,(+66)098*****19,***** ,ไทย,นนทบุรี,บางบัวทอง,,*******,*****************************************************,การเก็บเงินปลายทาง,0.1,เครื่องหอมในบ้าน,1161084358200165800 ,,Unchecked,,,,,,,,,
|
||||
578626692420044200 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1732333353752562036 ,P-SKU-OZO-THAI-SPA,น้ำมันหอมระเหยอโรม่า ขนาดมาตรฐาน ใช้ได้นาน กลิ่น ไทยสปา ซึมง่าย ไม่เหนียวเหนอะ,30 ml,1,0,79,79,0,0,79,45,45,0,0,,0,203,,28/04/2025 14:38:28 ,30/04/2025 18:39:16 ,29/04/2025 08:24:36 ,29/04/2025 14:30:51 ,30/04/2025 18:39:16 ," ",,,Fulfillment by seller,คลังหลัก,758329805192,การจัดส่งแบบมาตรฐาน,J&T Express,,n***ira246,น******** ศ********,(+66)098*****19,***** ,ไทย,นนทบุรี,บางบัวทอง,,*******,*****************************************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1161084358200165800 ,,Unchecked,,,,,,,,,
|
||||
578600949256849350 ,เสร็จสมบูรณ์,เสร็จสมบูรณ์,,Normal,1731215256170104180 ,30-ALL,น้ำหอมอโรม่า กลิ่น ลาเวนเดอร์ ขนาด 30 ml หยดในเครื่องพ่นไอน้ำ,กลิ่น เปปเปอร์มินต์,1,0,79,79,0,0,79,45,45,0,0,,0,124,,26/04/2025 12:23:51 ,28/04/2025 16:51:13 ,26/04/2025 19:14:56 ,27/04/2025 13:54:08 ,28/04/2025 16:51:13 ," ",,,Fulfillment by seller,คลังหลัก,758256115212,การจัดส่งแบบมาตรฐาน,J&T Express,,o***atongon160,O**********,(+66)824****35,***** ,ไทย,กรุงเทพฯ,คลองสามวา,,**************************************************************************************************************************,**************************************************************************************************************************,การเก็บเงินปลายทาง,0.12,เครื่องหอมในบ้าน,1160973103913732038 ,,Unchecked,,,,,,,,,
|
||||
|
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
/generated/prisma
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "iRelax API — Elysia.js + Prisma 7",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"start": "bun src/index.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.2.2",
|
||||
"@elysiajs/jwt": "^1.4.1",
|
||||
"@elysiajs/static": "^1.4.7",
|
||||
"@elysiajs/swagger": "^1.2.1",
|
||||
"@prisma/adapter-pg": "^7.6.0",
|
||||
"@prisma/client": "^7.6.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"dotenv": "^17.4.0",
|
||||
"elysia": "^1.2.25",
|
||||
"pg": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/pg": "^8.20.0",
|
||||
"prisma": "^7.6.0",
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "node prisma/seed.cjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Prisma 7 Config
|
||||
// Note: prisma CLI uses jiti (Node.js) to load this file, NOT Bun
|
||||
// So we must import dotenv explicitly here to load DATABASE_URL
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "bun prisma/seed.cjs",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
ผลิตภัณฑ์ Aroma Oil กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น
|
||||
@@ -0,0 +1 @@
|
||||
ผลิตภัณฑ์ Car Diffuser กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น
|
||||
@@ -0,0 +1 @@
|
||||
ผลิตภัณฑ์ Oil Ozone กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น
|
||||
@@ -0,0 +1 @@
|
||||
ผลิตภัณฑ์ Reed Diffuser กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น
|
||||
@@ -0,0 +1 @@
|
||||
ผลิตภัณฑ์ Room Spray กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น
|
||||
@@ -0,0 +1,4 @@
|
||||
รายละเอียดฉบับเต็มของ Aroma Oil กลิ่น {{scent}}
|
||||
- ส่วนประกอบนำเข้าคุณภาพสูง
|
||||
- กระจายความหอมได้ยาวนานในห้อง
|
||||
- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว
|
||||
@@ -0,0 +1,4 @@
|
||||
รายละเอียดฉบับเต็มของ Car Diffuser กลิ่น {{scent}}
|
||||
- ส่วนประกอบนำเข้าคุณภาพสูง
|
||||
- กระจายความหอมได้ยาวนานในห้อง
|
||||
- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว
|
||||
@@ -0,0 +1,4 @@
|
||||
รายละเอียดฉบับเต็มของ Oil Ozone กลิ่น {{scent}}
|
||||
- ส่วนประกอบนำเข้าคุณภาพสูง
|
||||
- กระจายความหอมได้ยาวนานในห้อง
|
||||
- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว
|
||||
@@ -0,0 +1,4 @@
|
||||
รายละเอียดฉบับเต็มของ Reed Diffuser กลิ่น {{scent}}
|
||||
- ส่วนประกอบนำเข้าคุณภาพสูง
|
||||
- กระจายความหอมได้ยาวนานในห้อง
|
||||
- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว
|
||||
@@ -0,0 +1,4 @@
|
||||
รายละเอียดฉบับเต็มของ Room Spray กลิ่น {{scent}}
|
||||
- ส่วนประกอบนำเข้าคุณภาพสูง
|
||||
- กระจายความหอมได้ยาวนานในห้อง
|
||||
- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว
|
||||
@@ -0,0 +1,94 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Scent" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Scent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Category" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Product" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"price" DOUBLE PRECISION DEFAULT 0,
|
||||
"categoryId" INTEGER NOT NULL,
|
||||
"scentId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProductImage" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"productId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "ProductImage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProductVariant" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"size" TEXT NOT NULL,
|
||||
"price" DOUBLE PRECISION DEFAULT 0,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProductVariant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'ADMIN',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Scent_name_key" ON "Scent"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Product_scentId_categoryId_key" ON "Product"("scentId", "categoryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProductVariant_productId_size_key" ON "ProductVariant"("productId", "size");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Product" ADD CONSTRAINT "Product_scentId_fkey" FOREIGN KEY ("scentId") REFERENCES "Scent"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProductImage" ADD CONSTRAINT "ProductImage_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ADD COLUMN "details" TEXT;
|
||||
@@ -0,0 +1,57 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Hero" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Hero_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreSettings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 35,
|
||||
"bankName" TEXT NOT NULL,
|
||||
"bankAccountNumber" TEXT NOT NULL,
|
||||
"bankAccountName" TEXT NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "StoreSettings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Order" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"customerName" TEXT NOT NULL,
|
||||
"customerPhone" TEXT NOT NULL,
|
||||
"customerEmail" TEXT NOT NULL,
|
||||
"shippingAddress" TEXT NOT NULL,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 35,
|
||||
"totalAmount" DOUBLE PRECISION NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING_PAYMENT',
|
||||
"paymentSlipUrl" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Order_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OrderItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderId" INTEGER NOT NULL,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"variantId" INTEGER NOT NULL,
|
||||
"productName" TEXT NOT NULL,
|
||||
"variantSize" TEXT NOT NULL,
|
||||
"productImage" TEXT,
|
||||
"quantity" INTEGER NOT NULL,
|
||||
"priceAtPurchase" DOUBLE PRECISION NOT NULL,
|
||||
|
||||
CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `description` to the `Hero` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `tagline` to the `Hero` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `title` to the `Hero` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Hero" ADD COLUMN "buttonLink" TEXT NOT NULL DEFAULT '/shop',
|
||||
ADD COLUMN "buttonText" TEXT NOT NULL DEFAULT 'Explore Collection',
|
||||
ADD COLUMN "description" TEXT NOT NULL,
|
||||
ADD COLUMN "tagline" TEXT NOT NULL,
|
||||
ADD COLUMN "title" TEXT NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "OrderItem" ALTER COLUMN "variantId" DROP NOT NULL,
|
||||
ALTER COLUMN "variantSize" DROP NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "StoreSettings" ALTER COLUMN "id" SET DEFAULT 1,
|
||||
ALTER COLUMN "id" DROP DEFAULT,
|
||||
ALTER COLUMN "bankName" SET DEFAULT 'KASIKORNBANK (KBANK)',
|
||||
ALTER COLUMN "bankAccountNumber" SET DEFAULT '012-3-45678-9',
|
||||
ALTER COLUMN "bankAccountName" SET DEFAULT 'iRelax Aroma Co., Ltd.';
|
||||
DROP SEQUENCE "StoreSettings_id_seq";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Article" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"coverImage" TEXT,
|
||||
"content" TEXT NOT NULL,
|
||||
"isPublished" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TiktokIncome" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"orderCreatedTime" TEXT,
|
||||
"orderSettledTime" TEXT,
|
||||
"currency" TEXT NOT NULL,
|
||||
"totalSettlementAmount" DOUBLE PRECISION NOT NULL,
|
||||
"totalRevenue" DOUBLE PRECISION NOT NULL,
|
||||
"totalFees" DOUBLE PRECISION NOT NULL,
|
||||
"rawData" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TiktokIncome_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TiktokProduct" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productName" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"gmv" DOUBLE PRECISION NOT NULL,
|
||||
"itemsSold" INTEGER NOT NULL,
|
||||
"orders" INTEGER NOT NULL,
|
||||
"rawData" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TiktokProduct_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TiktokProductSku" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"skuName" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"gmv" DOUBLE PRECISION NOT NULL,
|
||||
"skuOrders" INTEGER NOT NULL,
|
||||
"itemsSold" INTEGER NOT NULL,
|
||||
"rawData" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TiktokProductSku_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RawMaterial" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"unit" TEXT NOT NULL,
|
||||
"pricePerUnit" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "RawMaterial_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BomItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"productVariantId" INTEGER NOT NULL,
|
||||
"rawMaterialId" INTEGER NOT NULL,
|
||||
"quantity" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "BomItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ShopeeOrder" (
|
||||
"orderId" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL DEFAULT 'hom-aroma',
|
||||
"status" TEXT NOT NULL,
|
||||
"buyerUsername" TEXT,
|
||||
"orderCreatedAt" TEXT,
|
||||
"paymentTime" TEXT,
|
||||
"paymentMethod" TEXT,
|
||||
"trackingNo" TEXT,
|
||||
"totalAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"buyerPaidAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"commission" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"completedAt" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ShopeeOrder_pkey" PRIMARY KEY ("orderId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ShopeeOrderItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"parentSku" TEXT,
|
||||
"productName" TEXT NOT NULL,
|
||||
"skuRef" TEXT,
|
||||
"variantName" TEXT,
|
||||
"originalPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"dealPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
"totalPayment" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"rawData" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "ShopeeOrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TiktokOrder" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL DEFAULT 'hom-aroma',
|
||||
"status" TEXT NOT NULL,
|
||||
"subStatus" TEXT,
|
||||
"orderType" TEXT,
|
||||
"cancelBy" TEXT,
|
||||
"cancelReason" TEXT,
|
||||
"fulfillmentType" TEXT,
|
||||
"warehouseName" TEXT,
|
||||
"trackingId" TEXT,
|
||||
"deliveryOption" TEXT,
|
||||
"shippingProvider" TEXT,
|
||||
"buyerUsername" TEXT,
|
||||
"paymentMethod" TEXT,
|
||||
"orderAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"refundAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"originalShipping" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"taxes" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"province" TEXT,
|
||||
"district" TEXT,
|
||||
"country" TEXT,
|
||||
"createdTime" TEXT,
|
||||
"paidTime" TEXT,
|
||||
"rtsTime" TEXT,
|
||||
"shippedTime" TEXT,
|
||||
"deliveredTime" TEXT,
|
||||
"cancelledTime" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TiktokOrder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TiktokOrderItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"skuId" TEXT,
|
||||
"sellerSku" TEXT,
|
||||
"productName" TEXT NOT NULL,
|
||||
"variation" TEXT,
|
||||
"quantity" INTEGER NOT NULL DEFAULT 1,
|
||||
"skuQtyReturn" INTEGER NOT NULL DEFAULT 0,
|
||||
"unitOriginalPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"subtotalBeforeDiscount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"platformDiscount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"sellerDiscount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"subtotalAfterDiscount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"productCategory" TEXT,
|
||||
"weight" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "TiktokOrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Article_slug_key" ON "Article"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RawMaterial_name_key" ON "RawMaterial"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BomItem_productVariantId_rawMaterialId_key" ON "BomItem"("productVariantId", "rawMaterialId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TiktokOrder_orderId_key" ON "TiktokOrder"("orderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TiktokOrder_shop_idx" ON "TiktokOrder"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TiktokOrder_status_idx" ON "TiktokOrder"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TiktokOrder_createdTime_idx" ON "TiktokOrder"("createdTime");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BomItem" ADD CONSTRAINT "BomItem_productVariantId_fkey" FOREIGN KEY ("productVariantId") REFERENCES "ProductVariant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "BomItem" ADD CONSTRAINT "BomItem_rawMaterialId_fkey" FOREIGN KEY ("rawMaterialId") REFERENCES "RawMaterial"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ShopeeOrderItem" ADD CONSTRAINT "ShopeeOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "ShopeeOrder"("orderId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TiktokOrderItem" ADD CONSTRAINT "TiktokOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "TiktokOrder"("orderId") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,60 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "LazadaOrder" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderNumber" TEXT NOT NULL,
|
||||
"shop" TEXT NOT NULL DEFAULT 'hom-aroma',
|
||||
"createTime" TEXT,
|
||||
"updateTime" TEXT,
|
||||
"deliveredDate" TEXT,
|
||||
"customerName" TEXT,
|
||||
"shippingCity" TEXT,
|
||||
"shippingPostCode" TEXT,
|
||||
"shippingRegion" TEXT,
|
||||
"payMethod" TEXT,
|
||||
"status" TEXT,
|
||||
"trackingCode" TEXT,
|
||||
"shippingProvider" TEXT,
|
||||
"orderAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LazadaOrder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LazadaOrderItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"orderNumber" TEXT NOT NULL,
|
||||
"orderItemId" TEXT NOT NULL,
|
||||
"sellerSku" TEXT,
|
||||
"lazadaSku" TEXT,
|
||||
"itemName" TEXT NOT NULL,
|
||||
"variation" TEXT,
|
||||
"unitPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"paidPrice" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"sellerDiscountTotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"platformDiscountTotal" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"shippingFee" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"status" TEXT,
|
||||
|
||||
CONSTRAINT "LazadaOrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LazadaOrder_orderNumber_key" ON "LazadaOrder"("orderNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LazadaOrder_shop_idx" ON "LazadaOrder"("shop");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LazadaOrder_status_idx" ON "LazadaOrder"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LazadaOrder_createTime_idx" ON "LazadaOrder"("createTime");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LazadaOrderItem_orderItemId_key" ON "LazadaOrderItem"("orderItemId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LazadaOrderItem" ADD CONSTRAINT "LazadaOrderItem_orderNumber_fkey" FOREIGN KEY ("orderNumber") REFERENCES "LazadaOrder"("orderNumber") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Category" ADD COLUMN "imageUrl" TEXT;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "customerId" INTEGER;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Customer" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"lineUserId" TEXT NOT NULL,
|
||||
"displayName" TEXT,
|
||||
"pictureUrl" TEXT,
|
||||
"email" TEXT,
|
||||
"phone" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Customer_lineUserId_key" ON "Customer"("lineUserId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Order" ADD CONSTRAINT "Order_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Order" ADD COLUMN "couponCode" TEXT,
|
||||
ADD COLUMN "discountAmount" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Coupon" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"minPurchase" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"maxDiscount" DOUBLE PRECISION,
|
||||
"usageLimit" INTEGER,
|
||||
"usedCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Coupon_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Coupon_code_key" ON "Coupon"("code");
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,374 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model Scent {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
products Product[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
imageUrl String?
|
||||
products Product[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Product {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String?
|
||||
details String?
|
||||
price Float? @default(0)
|
||||
categoryId Int
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
scentId Int
|
||||
scent Scent @relation(fields: [scentId], references: [id])
|
||||
variants ProductVariant[]
|
||||
images ProductImage[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([scentId, categoryId])
|
||||
}
|
||||
|
||||
model ProductImage {
|
||||
id Int @id @default(autoincrement())
|
||||
url String
|
||||
order Int @default(0)
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model ProductVariant {
|
||||
id Int @id @default(autoincrement())
|
||||
size String
|
||||
price Float? @default(0)
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
bomItems BomItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productId, size])
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String
|
||||
role String @default("ADMIN")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id Int @id @default(autoincrement())
|
||||
lineUserId String @unique
|
||||
displayName String?
|
||||
pictureUrl String?
|
||||
email String?
|
||||
phone String?
|
||||
orders Order[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Hero {
|
||||
id Int @id @default(autoincrement())
|
||||
tagline String
|
||||
title String
|
||||
description String
|
||||
imageUrl String
|
||||
buttonText String @default("Explore Collection")
|
||||
buttonLink String @default("/shop")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Coupon {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
type String // "PERCENTAGE", "FIXED_AMOUNT", "FREE_SHIPPING"
|
||||
value Float
|
||||
minPurchase Float @default(0)
|
||||
maxDiscount Float?
|
||||
usageLimit Int?
|
||||
usedCount Int @default(0)
|
||||
expiresAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model StoreSettings {
|
||||
id Int @id @default(1)
|
||||
shippingFee Float @default(35)
|
||||
bankName String @default("KASIKORNBANK (KBANK)")
|
||||
bankAccountName String @default("iRelax Aroma Co., Ltd.")
|
||||
bankAccountNumber String @default("012-3-45678-9")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Order {
|
||||
id Int @id @default(autoincrement())
|
||||
customerName String
|
||||
customerEmail String
|
||||
customerPhone String
|
||||
shippingAddress String
|
||||
totalAmount Float
|
||||
shippingFee Float @default(35)
|
||||
couponCode String?
|
||||
discountAmount Float @default(0)
|
||||
status String @default("PENDING_PAYMENT")
|
||||
paymentSlipUrl String?
|
||||
|
||||
customerId Int?
|
||||
customer Customer? @relation(fields: [customerId], references: [id])
|
||||
|
||||
items OrderItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId Int
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
productId Int
|
||||
variantId Int?
|
||||
quantity Int
|
||||
priceAtPurchase Float
|
||||
productName String
|
||||
variantSize String?
|
||||
productImage String?
|
||||
}
|
||||
|
||||
model Article {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique
|
||||
title String
|
||||
coverImage String?
|
||||
content String
|
||||
isPublished Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TiktokIncome {
|
||||
id String @id // Order/adjustment ID
|
||||
type String
|
||||
orderCreatedTime String?
|
||||
orderSettledTime String?
|
||||
currency String
|
||||
totalSettlementAmount Float
|
||||
totalRevenue Float
|
||||
totalFees Float
|
||||
rawData Json // Stores all other 50+ columns
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TiktokProduct {
|
||||
id String @id // Product ID
|
||||
productName String
|
||||
status String
|
||||
gmv Float
|
||||
itemsSold Int
|
||||
orders Int
|
||||
rawData Json // Stores remaining tab/live/video metrics
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TiktokProductSku {
|
||||
id String @id // SKU ID
|
||||
productId String
|
||||
skuName String
|
||||
status String
|
||||
gmv Float
|
||||
skuOrders Int
|
||||
itemsSold Int
|
||||
rawData Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model RawMaterial {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
unit String // e.g., 'kg', 'liter', 'piece'
|
||||
pricePerUnit Float
|
||||
bomItems BomItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model BomItem {
|
||||
id Int @id @default(autoincrement())
|
||||
productVariantId Int
|
||||
productVariant ProductVariant @relation(fields: [productVariantId], references: [id], onDelete: Cascade)
|
||||
rawMaterialId Int
|
||||
rawMaterial RawMaterial @relation(fields: [rawMaterialId], references: [id], onDelete: Cascade)
|
||||
quantity Float
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productVariantId, rawMaterialId])
|
||||
}
|
||||
|
||||
model ShopeeOrder {
|
||||
orderId String @id
|
||||
shop String @default("hom-aroma")
|
||||
status String
|
||||
buyerUsername String?
|
||||
orderCreatedAt String?
|
||||
paymentTime String?
|
||||
paymentMethod String?
|
||||
trackingNo String?
|
||||
totalAmount Float @default(0)
|
||||
buyerPaidAmount Float @default(0)
|
||||
shippingFee Float @default(0)
|
||||
commission Float @default(0)
|
||||
completedAt String?
|
||||
items ShopeeOrderItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ShopeeOrderItem {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String
|
||||
order ShopeeOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
|
||||
parentSku String?
|
||||
productName String
|
||||
skuRef String?
|
||||
variantName String?
|
||||
originalPrice Float @default(0)
|
||||
dealPrice Float @default(0)
|
||||
quantity Int @default(1)
|
||||
totalPayment Float @default(0)
|
||||
rawData Json
|
||||
}
|
||||
|
||||
// ── TikTok Orders ──────────────────────────────────────────────────────────────
|
||||
model TiktokOrder {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String @unique
|
||||
shop String @default("hom-aroma")
|
||||
status String // Order Status (Thai)
|
||||
subStatus String? // Order Substatus
|
||||
orderType String? // Normal / Pre-order
|
||||
cancelBy String?
|
||||
cancelReason String?
|
||||
fulfillmentType String?
|
||||
warehouseName String?
|
||||
trackingId String?
|
||||
deliveryOption String?
|
||||
shippingProvider String?
|
||||
buyerUsername String?
|
||||
paymentMethod String?
|
||||
// Financials (order-level)
|
||||
orderAmount Float @default(0) // Total paid by buyer
|
||||
refundAmount Float @default(0)
|
||||
shippingFee Float @default(0) // Shipping Fee After Discount
|
||||
originalShipping Float @default(0)
|
||||
taxes Float @default(0)
|
||||
// Geography
|
||||
province String?
|
||||
district String?
|
||||
country String?
|
||||
// Timestamps
|
||||
createdTime String?
|
||||
paidTime String?
|
||||
rtsTime String?
|
||||
shippedTime String?
|
||||
deliveredTime String?
|
||||
cancelledTime String?
|
||||
|
||||
items TiktokOrderItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([shop])
|
||||
@@index([status])
|
||||
@@index([createdTime])
|
||||
}
|
||||
|
||||
model TiktokOrderItem {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String
|
||||
order TiktokOrder @relation(fields: [orderId], references: [orderId], onDelete: Cascade)
|
||||
skuId String?
|
||||
sellerSku String?
|
||||
productName String
|
||||
variation String?
|
||||
quantity Int @default(1)
|
||||
skuQtyReturn Int @default(0)
|
||||
unitOriginalPrice Float @default(0)
|
||||
subtotalBeforeDiscount Float @default(0)
|
||||
platformDiscount Float @default(0)
|
||||
sellerDiscount Float @default(0)
|
||||
subtotalAfterDiscount Float @default(0) // Revenue ระดับ SKU
|
||||
productCategory String?
|
||||
weight Float @default(0)
|
||||
}
|
||||
|
||||
// ── Lazada Orders ──────────────────────────────────────────────────────────────
|
||||
model LazadaOrder {
|
||||
id Int @id @default(autoincrement())
|
||||
orderNumber String @unique
|
||||
shop String @default("hom-aroma")
|
||||
createTime String?
|
||||
updateTime String?
|
||||
deliveredDate String?
|
||||
customerName String?
|
||||
shippingCity String?
|
||||
shippingPostCode String?
|
||||
shippingRegion String?
|
||||
payMethod String?
|
||||
status String? // Based on items or first item
|
||||
trackingCode String?
|
||||
shippingProvider String?
|
||||
// Aggregate from items
|
||||
orderAmount Float @default(0) // sum(paidPrice)
|
||||
shippingFee Float @default(0) // sum(shippingFee)
|
||||
|
||||
items LazadaOrderItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([shop])
|
||||
@@index([status])
|
||||
@@index([createTime])
|
||||
}
|
||||
|
||||
model LazadaOrderItem {
|
||||
id Int @id @default(autoincrement())
|
||||
orderNumber String
|
||||
order LazadaOrder @relation(fields: [orderNumber], references: [orderNumber], onDelete: Cascade)
|
||||
orderItemId String @unique
|
||||
sellerSku String?
|
||||
lazadaSku String?
|
||||
itemName String
|
||||
variation String?
|
||||
unitPrice Float @default(0)
|
||||
paidPrice Float @default(0)
|
||||
sellerDiscountTotal Float @default(0)
|
||||
platformDiscountTotal Float @default(0)
|
||||
shippingFee Float @default(0)
|
||||
status String?
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// seed.cjs - Prisma 7 Seed Script
|
||||
// Note: Bun loads .env automatically - no need for dotenv
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { PrismaPg } = require('@prisma/adapter-pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const scentNames = [
|
||||
'ตะไคร้', 'ลาเวนเดอร์', 'วานิลลา', 'ซากุระ', 'มะลิ', 'ยูคาลิปตัส', 'เฟรชลาเวนเดอร์',
|
||||
'เปเปอร์มิ้น', 'โอเชี่ยน', 'เซเว่นซี', 'ดอกแก้ว', 'ดอกโมก', 'ลีลาวดี', 'ส้ม', 'มะนาว',
|
||||
'แคนตาลูป', 'แอปเปิ้ลแดง', 'แอปเปิ้ลเขียว', 'ดอกลิลลี่', 'โรสเมรี่', 'อัญชัน',
|
||||
'กุหลาบแดง', 'หมากฝรั่ง', 'ผลไม้รวม', 'มะกรูด', 'แซนด้าวูส', 'ดอกปีบ', 'ดอกบัว',
|
||||
'โอโซน', 'ไทยสปา', 'กล้วย', 'กาแฟ', 'ดอกชมนาด', 'กลิ่นน้ำนม', 'ดอกการะเวก',
|
||||
'ดอกจำปา', 'ดอกพิกุล', 'ดอกพุด', 'ดอกราตรี', 'ดอกลำดวน', 'ทับทิม', 'มะละกอสุก',
|
||||
'ดอกราชาวดี', 'กลิ่นลูกพีช', 'กลิ่นองุ่น'
|
||||
];
|
||||
|
||||
const categoryConfigs = [
|
||||
{
|
||||
name: 'Oil Ozone',
|
||||
sizes: ['30ml', '60ml', '85ml', '120ml'],
|
||||
priceMap: { '30ml': 69, '60ml': 100, '85ml': 120, '120ml': 150 }
|
||||
},
|
||||
{
|
||||
name: 'Aroma Oil',
|
||||
sizes: ['30ml', '60ml', '85ml', '120ml'],
|
||||
priceMap: { '30ml': 90, '60ml': 150, '85ml': 165, '120ml': 225 } // 120ml ปรับเป็น 225 (฿1.875/ml) ให้ถูกกว่า 85ml ต่อ ml
|
||||
},
|
||||
{
|
||||
name: 'Reed Diffuser',
|
||||
sizes: ['60ml', '85ml', '120ml'],
|
||||
priceMap: { '60ml': 120, '85ml': 160, '120ml': 190 }
|
||||
},
|
||||
{
|
||||
name: 'Room Spray',
|
||||
sizes: ['85ml'],
|
||||
priceMap: { '85ml': 150 }
|
||||
},
|
||||
{
|
||||
name: 'Car Diffuser',
|
||||
sizes: ['25ml'],
|
||||
priceMap: { '25ml': 60 }
|
||||
}
|
||||
];
|
||||
|
||||
async function main() {
|
||||
process.stdout.write('🚀 Starting Advanced Seeding (Products & Variants)...\n');
|
||||
|
||||
// Load or create category description files
|
||||
const descriptionsDir = path.join(__dirname, 'descriptions');
|
||||
if (!fs.existsSync(descriptionsDir)) {
|
||||
fs.mkdirSync(descriptionsDir);
|
||||
}
|
||||
|
||||
const categoryDescriptions = {};
|
||||
for (const config of categoryConfigs) {
|
||||
const safeName = config.name.replace(/\s+/g, '_');
|
||||
const descPath = path.join(descriptionsDir, `${safeName}.txt`);
|
||||
|
||||
// Auto-create with default template if not exists
|
||||
if (!fs.existsSync(descPath)) {
|
||||
const defaultDesc = `ผลิตภัณฑ์ ${config.name} กลิ่น {{scent}} มอบประสบการณ์ความหอมที่ผ่อนคลายและสดชื่น ช่วยให้บรรยากาศในห้องของคุณน่าอยู่มากยิ่งขึ้น`;
|
||||
fs.writeFileSync(descPath, defaultDesc, 'utf8');
|
||||
}
|
||||
|
||||
categoryDescriptions[config.name] = fs.readFileSync(descPath, 'utf8');
|
||||
|
||||
// Auto-create category details
|
||||
const detailsDir = path.join(__dirname, 'details');
|
||||
if (!fs.existsSync(detailsDir)) {
|
||||
fs.mkdirSync(detailsDir);
|
||||
}
|
||||
const detailPath = path.join(detailsDir, `${safeName}.txt`);
|
||||
if (!fs.existsSync(detailPath)) {
|
||||
const defaultDetail = `รายละเอียดฉบับเต็มของ ${config.name} กลิ่น {{scent}}\n- ส่วนประกอบนำเข้าคุณภาพสูง\n- กระจายความหอมได้ยาวนานในห้อง\n- ปลอดภัยต่อสุขภาพและเหมาะสำหรับทุกคนในครอบครัว`;
|
||||
fs.writeFileSync(detailPath, defaultDetail, 'utf8');
|
||||
}
|
||||
categoryConfigs[config.name] = categoryConfigs[config.name] || {};
|
||||
categoryConfigs[config.name].detailsTemplate = fs.readFileSync(detailPath, 'utf8');
|
||||
}
|
||||
|
||||
// 0. Seed Admin User
|
||||
const adminPassword = await bcrypt.hash('irelax123', 10);
|
||||
await prisma.user.upsert({
|
||||
where: { username: 'admin' },
|
||||
update: { password: adminPassword },
|
||||
create: {
|
||||
username: 'admin',
|
||||
password: adminPassword,
|
||||
role: 'ADMIN'
|
||||
}
|
||||
});
|
||||
process.stdout.write('✅ Seeded Admin User (admin / irelax123).\n');
|
||||
|
||||
// 1. Seed Scents & Categories
|
||||
const scents = [];
|
||||
for (const name of scentNames) {
|
||||
const s = await prisma.scent.upsert({
|
||||
where: { name },
|
||||
update: {},
|
||||
create: { name }
|
||||
});
|
||||
scents.push(s);
|
||||
}
|
||||
process.stdout.write(`✅ Seeded ${scents.length} Scents.\n`);
|
||||
|
||||
const categories = [];
|
||||
for (const config of categoryConfigs) {
|
||||
const c = await prisma.category.upsert({
|
||||
where: { name: config.name },
|
||||
update: {},
|
||||
create: { name: config.name }
|
||||
});
|
||||
categories.push({ ...c, allowedSizes: config.sizes, priceMap: config.priceMap, detailsTemplate: categoryConfigs[config.name].detailsTemplate });
|
||||
}
|
||||
process.stdout.write(`✅ Seeded ${categories.length} Categories.\n`);
|
||||
|
||||
// 2. Cross Scents x Categories to create Products
|
||||
process.stdout.write('📦 Generating Products and Variants...\n');
|
||||
let productCount = 0;
|
||||
let variantCount = 0;
|
||||
|
||||
for (const scent of scents) {
|
||||
for (const category of categories) {
|
||||
const productName = `${scent.name} ${category.name}`;
|
||||
|
||||
// Get category description template and replace {{scent}}
|
||||
let productDescription = categoryDescriptions[category.name] || `Product ${scent.name} in category ${category.name}`;
|
||||
productDescription = productDescription.replace(/{{scent}}/g, scent.name);
|
||||
|
||||
let productDetails = category.detailsTemplate || '';
|
||||
productDetails = productDetails.replace(/{{scent}}/g, scent.name);
|
||||
|
||||
// Upsert Product
|
||||
const product = await prisma.product.upsert({
|
||||
where: {
|
||||
scentId_categoryId: {
|
||||
scentId: scent.id,
|
||||
categoryId: category.id
|
||||
}
|
||||
},
|
||||
update: {
|
||||
name: productName,
|
||||
description: productDescription,
|
||||
details: productDetails
|
||||
},
|
||||
create: {
|
||||
name: productName,
|
||||
scentId: scent.id,
|
||||
categoryId: category.id,
|
||||
description: productDescription,
|
||||
details: productDetails
|
||||
}
|
||||
});
|
||||
productCount++;
|
||||
|
||||
// 3. Create Default Product Image
|
||||
// Using existing category-based image paths as initial seed data
|
||||
const defaultImagePath = category.name === 'Oil Ozone' || category.name === 'Aroma Oil' || category.name === 'Car Diffuser'
|
||||
? '/images/oil_ozone.png'
|
||||
: '/images/reed_diffuser.png';
|
||||
|
||||
await prisma.productImage.upsert({
|
||||
where: { id: (product.id * 10) + 1 }, // Pseudo-stable ID for seed
|
||||
update: { url: defaultImagePath },
|
||||
create: {
|
||||
id: (product.id * 10) + 1,
|
||||
productId: product.id,
|
||||
url: defaultImagePath,
|
||||
order: 0
|
||||
}
|
||||
}).catch(() => {
|
||||
// Fallback for cases where ID might conflict or upsert on non-unique field
|
||||
return prisma.productImage.create({
|
||||
data: {
|
||||
productId: product.id,
|
||||
url: defaultImagePath,
|
||||
order: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 4. Create Variants (Sizes) with prices from priceMap
|
||||
for (const size of category.allowedSizes) {
|
||||
const price = category.priceMap?.[size] ?? 0;
|
||||
await prisma.productVariant.upsert({
|
||||
where: {
|
||||
productId_size: {
|
||||
productId: product.id,
|
||||
size: size
|
||||
}
|
||||
},
|
||||
update: { price }, // อัปเดตราคาเมื่อ re-seed
|
||||
create: {
|
||||
productId: product.id,
|
||||
size: size,
|
||||
price: price
|
||||
}
|
||||
});
|
||||
variantCount++;
|
||||
}
|
||||
}
|
||||
process.stdout.write(`.`); // Progress indicator
|
||||
}
|
||||
|
||||
process.stdout.write(`\n✨ Seeding Complete!\n`);
|
||||
process.stdout.write(`📊 Summary:\n`);
|
||||
process.stdout.write(` - Scents: ${scents.length}\n`);
|
||||
process.stdout.write(` - Categories: ${categories.length}\n`);
|
||||
process.stdout.write(` - Products: ${productCount}\n`);
|
||||
process.stdout.write(` - Variants: ${variantCount}\n`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
process.stderr.write(`❌ Error: ${e.message}\n`);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 552 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 1.3 MiB |