The application exposes RESTful API endpoints built with Next.js App Router and backed by Supabase. All routes use the Supabase Admin client (bypassing Row-Level Security) to perform full CRUD operations.
| Environment | URL |
|---|---|
| Local development | http://localhost:3000/api |
The following 20 entities have standard CRUD API endpoints:
| Entity | Table Name |
|---|---|
| Booking | booking |
| Booking Detail | booking_detail |
| Coupon | coupon |
| Customer | customer |
| Employee | employee |
| Leave Record | leave_record |
| Massage | massage |
| Member Coupon | member_coupon |
| Member Package | member_package |
| Operate Time | operate_time |
| Package | package |
| Package Detail | package_detail |
| Package Order | package_order |
| Payment | payment |
| Profiles | profiles |
| Room | room |
| Room Massage | room_massage |
| Therapist Massage Skill | therapist_massage_skill |
| Users | users |
| Work Schedule | work_schedule |
Every entity supports the following operations:
GET /api/[entity]
Response 200:
{
"success": true,
"data": [ ... ]
}POST /api/[entity]
Headers: Content-Type: application/json
Body: JSON object matching the entity's schema.
Response 201:
{
"success": true,
"data": { ...insertedRecord }
}GET /api/[entity]/[id]
Response 200:
{
"success": true,
"data": { ...record }
}PUT /api/[entity]/[id]
Headers: Content-Type: application/json
Body: JSON object with the fields to update.
Response 200:
{
"success": true,
"data": { ...updatedRecord }
}DELETE /api/[entity]/[id]
Response 200:
{
"success": true,
"message": "[entity] deleted successfully"
}All endpoints return a consistent error shape when something goes wrong (missing ID, database error, invalid payload, etc.). Always check the success field.
{
"success": false,
"error": "Descriptive error message"
}The examples below use the customer entity with the JavaScript fetch API.
const res = await fetch('/api/customer');
const { data } = await res.json();
console.log(data); // Customer[]const res = await fetch('/api/customer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
phone: '1234567890',
}),
});
const { data } = await res.json();const res = await fetch(`/api/customer/${customerId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: '0987654321' }),
});const res = await fetch(`/api/customer/${customerId}`, { method: 'DELETE' });
const { message } = await res.json();| Field | Type | Required | Description |
|---|---|---|---|
massage_id |
int |
Auto | Primary key (auto-increment) |
massage_name |
text |
✅ | Name of the massage service |
massage_price |
numeric |
✅ | Price in Thai Baht |
massage_time |
int |
❌ | Duration in minutes (e.g. 60, 90, 120) |
image_src |
text |
❌ | Public URL to the massage image |
Example POST body:
{
"massage_name": "นวดน้ำมันหอมระเหย (Aromatherapy Oil Massage)",
"massage_price": 850.00,
"massage_time": 90,
"image_src": "https://<project>.supabase.co/storage/v1/object/public/massage-images/aromatherapy.jpg"
}| Field | Type | Required | Description |
|---|---|---|---|
operate_time_id |
int |
Auto | Primary key (auto-increment) |
open_time |
time |
✅ | Shop opening time (e.g. "09:00:00") |
close_time |
time |
✅ | Shop closing time (e.g. "21:00:00") |
create_date |
timestamptz |
Auto | Timestamp of record creation |
Example POST body:
{
"open_time": "10:00:00",
"close_time": "22:00:00"
}| Field | Type | Required | Description |
|---|---|---|---|
package_id |
int |
Auto | Primary key (auto-increment) |
package_name |
text |
✅ | Name of the package |
package_price |
numeric |
✅ | Total price of the package |
campaign_start_dateTime |
timestamptz |
❌ | Start of the promotion |
campaign_end_dateTime |
timestamptz |
❌ | End of the promotion |
image_src |
text |
❌ | URL to the package image |
| Field | Type | Required | Description |
|---|---|---|---|
coupon_id |
int |
Auto | Primary key (auto-increment) |
coupon_name |
text |
✅ | Name of the coupon |
discount_percent |
numeric |
✅ | Discount percentage (e.g. 10.00) |
description |
text |
❌ | Description of the coupon |
collect_deadline |
timestamptz |
❌ | Last day the coupon can be collected |
Note on
GET /api/coupon: By default only coupons wherecollect_deadlineisnullor in the future are returned. To include expired coupons (e.g. for manager views), append?show_all=true.
| Field | Type | Required | Description |
|---|---|---|---|
package_order_id |
int |
Auto | Primary key (auto-increment) |
customer_id |
int |
✅ | FK → customer.customer_id |
package_id |
int |
✅ | FK → package.package_id |
order_dateTime |
timestamptz |
Auto | Timestamp of the order |
total_price |
numeric |
✅ | Total amount charged |
payment_status |
text |
Auto | Payment status (pending, completed, etc.) |
Note: Use the specialized
POST /api/package_orderendpoint (see below) to create a package order — it atomically creates the order, the payment record, and themember_packageentitlements in a single transaction.
The following endpoints go beyond basic CRUD and encapsulate business logic. They require authentication (via Supabase session cookie) unless stated otherwise.
GET /api/booking/history
Returns all past bookings for the currently authenticated customer, with nested payment, booking detail, massage, therapist, and room data.
Auth required: Yes
Response 200:
{
"success": true,
"data": [
{
"booking_id": 1,
"booking_dateTime": "2026-03-01T10:00:00+07:00",
"payment": { "payment_status": "completed", "amount": 850 },
"booking_detail": [ { "massage": { "massage_name": "..." }, ... } ]
}
]
}GET /api/dashboard
GET /api/dashboard?from=2026-01-01&to=2026-03-31
Returns a comprehensive analytics payload for the manager dashboard. Supports an optional date range (from / to in YYYY-MM-DD format; from defaults to the earliest possible date, to defaults to today).
Auth required: No (admin-client; protect at the route/layout level)
Query params:
| Parameter | Format | Description |
|---|---|---|
from |
YYYY-MM-DD |
Start of the date range (inclusive) |
to |
YYYY-MM-DD |
End of the date range (inclusive) |
Response 200 payload includes:
- KPIs:
totalRevenue,totalBookings,newCustomersThisMonth,avgTransactionValue,availableTherapists - Period-over-period comparisons:
prevTotalRevenue,prevTotalBookings - Charts:
revenueByDay,popularServices,peakHours,roomUsage,therapistUtilization - Stats:
couponRedemption,packageSalesUsage - Live boards:
therapistStatus,roomStatus
GET /api/member_package/unused?customer_id=[id]
Returns all unused (and non-expired) member_package entitlements for a given customer, with nested package_detail, package, and massage data.
Auth required: No (caller must supply customer_id)
Query params:
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
int |
✅ | The customer whose unused packages to return |
Response 200:
{
"success": true,
"data": [
{
"member_package_id": 5,
"is_used": false,
"massage": { "massage_name": "Swedish Massage", "massage_time": 60 },
"package": { "package_name": "Relaxation Bundle" }
}
]
}DELETE /api/package_detail/by-package/[package_id]
Bulk-deletes all package_detail rows associated with the given package_id. Used internally when a manager replaces a package's service list during editing.
Auth required: No (protect at layout level)
Response 200:
{ "success": true, "message": "package details cleared successfully" }POST /api/package_order
Atomically purchases a package: creates a package_order record, a linked payment record, and one member_package entitlement per service in the package.
Auth required: No (caller supplies customer_id)
Headers: Content-Type: application/json
Body:
{
"customer_id": 42,
"package_id": 3,
"total_price": 1500.00,
"payment_method": "cash",
"payment_slip_url": "https://..."
}Response 201:
{ "success": true, "data": { "package_order_id": 7 } }GET /api/profile
PUT /api/profile
Gets or updates the customer profile associated with the currently authenticated Supabase session.
Auth required: Yes
PUT body (updatable fields only):
{
"first_name": "สมชาย",
"last_name": "ใจดี",
"phone_number": "0812345678"
}Response 200:
{ "success": true, "data": { ...customerRecord } }Note:
phone_numbermust be 9–10 digits (hyphens/spaces are stripped automatically).
POST /api/upload/massage-image
Uploads an image file to the massage-images Supabase Storage bucket and returns the public URL. Used by the manager UI when creating or editing a massage service.
Auth required: No (protect at layout level)
Headers: Content-Type: multipart/form-data
Body: FormData with a file field containing the image file.
Response 201:
{ "success": true, "url": "https://<project>.supabase.co/storage/v1/object/public/massage-images/..." }