obsidian 1
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"footnotes": false,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": true,
|
||||
"bases": true,
|
||||
"webviewer": false
|
||||
}
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "118cffd7cdee3b11",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "83169ae72c9bc357",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "6f192f9477d55538",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Crussell/Crussell Nails.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Crussell Nails"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "53a883d203e9233d",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "490f9de080de140d",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "42aca4a7881f98cd",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3f5a2398495786b0",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "332fce93850753ed",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300
|
||||
},
|
||||
"right": {
|
||||
"id": "2750d7726f904ef3",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "8cf8aee55a46981b",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "5c7ce50806d60318",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "Crussell/Crussell Nails.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for Crussell Nails"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "77c2a28112c134e9",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "Crussell/Crussell Nails.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from Crussell Nails"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0f217e57cf9fb6d9",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "013dd65f19607fbd",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "Crussell/Crussell Nails.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of Crussell Nails"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false,
|
||||
"bases:Create new base": false
|
||||
}
|
||||
},
|
||||
"active": "6f192f9477d55538",
|
||||
"lastOpenFiles": [
|
||||
"Crussell/Backend/bookings.md",
|
||||
"Crussell/Crussell Nails.md"
|
||||
]
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"nativeMenus": false
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"file-explorer": true,
|
||||
"global-search": true,
|
||||
"switcher": true,
|
||||
"graph": true,
|
||||
"backlink": true,
|
||||
"canvas": true,
|
||||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"footnotes": false,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
"command-palette": true,
|
||||
"slash-command": false,
|
||||
"editor-status": true,
|
||||
"bookmarks": true,
|
||||
"markdown-importer": false,
|
||||
"zk-prefixer": false,
|
||||
"random-note": false,
|
||||
"outline": true,
|
||||
"word-count": true,
|
||||
"slides": false,
|
||||
"audio-recorder": false,
|
||||
"workspaces": false,
|
||||
"file-recovery": true,
|
||||
"publish": false,
|
||||
"sync": true,
|
||||
"bases": true,
|
||||
"webviewer": false
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"collapse-filter": true,
|
||||
"search": "",
|
||||
"showTags": false,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": true,
|
||||
"collapse-color-groups": true,
|
||||
"colorGroups": [],
|
||||
"collapse-display": true,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 1,
|
||||
"lineSizeMultiplier": 1,
|
||||
"collapse-forces": true,
|
||||
"centerStrength": 0.518713248970312,
|
||||
"repelStrength": 10,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 1,
|
||||
"close": true
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.16.1",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://excalidraw-obsidian.online",
|
||||
"fundingUrl": "https://ko-fi.com/zsolt",
|
||||
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "obsidian-kanban",
|
||||
"name": "Kanban",
|
||||
"version": "2.0.51",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Create markdown-backed Kanban boards in Obsidian.",
|
||||
"author": "mgmeyers",
|
||||
"authorUrl": "https://github.com/mgmeyers/obsidian-kanban",
|
||||
"helpUrl": "https://publish.obsidian.md/kanban/Obsidian+Kanban+Plugin",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+179
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"main": {
|
||||
"id": "c62d1e0ec7a60717",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "d3b27c1f4c52d7a6",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "78ea01679f328555",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "Crussell Nails.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "Crussell Nails"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "vertical"
|
||||
},
|
||||
"left": {
|
||||
"id": "8bed8be933c79f20",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "516959ec91abc174",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "0f6fde9af78dd264",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "file-explorer",
|
||||
"state": {
|
||||
"sortOrder": "alphabetical",
|
||||
"autoReveal": false
|
||||
},
|
||||
"icon": "lucide-folder-closed",
|
||||
"title": "Files"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "356f344c2f4f81e6",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "search",
|
||||
"state": {
|
||||
"query": "",
|
||||
"matchingCase": false,
|
||||
"explainSearch": false,
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical"
|
||||
},
|
||||
"icon": "lucide-search",
|
||||
"title": "Search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bf7cf19fe90f2f37",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "bookmarks",
|
||||
"state": {},
|
||||
"icon": "lucide-bookmark",
|
||||
"title": "Bookmarks"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"right": {
|
||||
"id": "ae3733891099a958",
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "132c2d38d8dc839a",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "0078a932529c043e",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "backlink",
|
||||
"state": {
|
||||
"file": "Welcome.md",
|
||||
"collapseAll": false,
|
||||
"extraContext": false,
|
||||
"sortOrder": "alphabetical",
|
||||
"showSearch": false,
|
||||
"searchQuery": "",
|
||||
"backlinkCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-coming-in",
|
||||
"title": "Backlinks for Welcome"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c07138738d9a2d61",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outgoing-link",
|
||||
"state": {
|
||||
"file": "Welcome.md",
|
||||
"linksCollapsed": false,
|
||||
"unlinkedCollapsed": true
|
||||
},
|
||||
"icon": "links-going-out",
|
||||
"title": "Outgoing links from Welcome"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "23f7b34ce74479f8",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "tag",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"useHierarchy": true,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-tags",
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ec428ed62aa1165b",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "Welcome.md",
|
||||
"followCursor": false,
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Outline of Welcome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300,
|
||||
"collapsed": true
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
"switcher:Open quick switcher": false,
|
||||
"graph:Open graph view": false,
|
||||
"canvas:Create new canvas": false,
|
||||
"daily-notes:Open today's daily note": false,
|
||||
"templates:Insert template": false,
|
||||
"command-palette:Open command palette": false,
|
||||
"bases:Create new base": false
|
||||
}
|
||||
},
|
||||
"active": "78ea01679f328555",
|
||||
"lastOpenFiles": [
|
||||
"Untitled.base",
|
||||
"Crussell Nails.md",
|
||||
"2025-10-07.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
# Booking API - cURL Examples (Revised)
|
||||
|
||||
## User Endpoints (Requires Authentication)
|
||||
|
||||
-----
|
||||
|
||||
### 1\. Create Booking
|
||||
|
||||
Creates a new booking with the specified services and preferred time.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/bookings \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"service_ids": ["service-uuid-1", "service-uuid-2"],
|
||||
"notes": "Please use the side entrance"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "pending",
|
||||
"notes": "Please use the side entrance",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:30:00Z",
|
||||
"created_by": "user-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 2\. Get User Booking by ID
|
||||
|
||||
Retrieves a specific booking for the authenticated user.
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/bookings/booking-uuid \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
*The response now consistently includes all joined data: `services` and `payments`.*
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "pending",
|
||||
"notes": "Please use the side entrance",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:30:00Z",
|
||||
"created_by": "user-uuid",
|
||||
"services": [
|
||||
{
|
||||
"booking_id": "booking-uuid",
|
||||
"service_id": "service-uuid-1",
|
||||
"override_price": null,
|
||||
"override_duration_minutes": null,
|
||||
"service_name": "Haircut",
|
||||
"base_price": 25.00
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"id": "payment-uuid",
|
||||
"booking_id": "booking-uuid",
|
||||
"payment_type": "deposit",
|
||||
"payment_method": "card",
|
||||
"status": "completed",
|
||||
"amount": 10.00,
|
||||
"is_vat_applicable": true,
|
||||
"vat_rate": 20.0,
|
||||
"vat_amount": 2.00,
|
||||
"net_amount": 8.00
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 3\. Get All User Bookings
|
||||
|
||||
Retrieves a list of all bookings associated with the authenticated user.
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/bookings \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "booking-uuid-1",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "pending",
|
||||
"updated_at": "2025-10-21T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": "booking-uuid-2",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-26T10:00:00Z",
|
||||
"status": "confirmed",
|
||||
"updated_at": "2025-10-22T09:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 4\. Edit Booking (Change Start Time and/or Notes)
|
||||
|
||||
Updates the `start_time` and/or `notes` for a booking.
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:8080/api/bookings/booking-uuid \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"start_time": "2025-10-25T15:00:00Z",
|
||||
"notes": "Please use the front entrance this time"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T15:00:00Z",
|
||||
"status": "pending",
|
||||
"notes": "Please use the front entrance this time",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:40:00Z",
|
||||
"created_by": "user-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 5\. Delete/Cancel Booking
|
||||
|
||||
This endpoint performs a **hard delete** if no payments exist, or a **soft delete (cancel)** if payments exist, requiring a `reason`.
|
||||
|
||||
#### A. Hard Delete (No Payments)
|
||||
|
||||
```bash
|
||||
# Hard delete when no payments exist
|
||||
curl -X DELETE http://localhost:8080/api/bookings/booking-uuid \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Booking deleted successfully",
|
||||
"id": "booking-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Cancel Booking (With Payments)
|
||||
|
||||
```bash
|
||||
# Soft delete/cancel when payments exist - requires reason
|
||||
curl -X DELETE http://localhost:8080/api/bookings/booking-uuid \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-d '{
|
||||
"reason": "client_cancelled"
|
||||
}'
|
||||
```
|
||||
|
||||
**Valid reasons:** `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Booking cancelled successfully",
|
||||
"id": "booking-uuid",
|
||||
"status": "client_cancelled"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
-----
|
||||
|
||||
## Admin Endpoints (Requires Authentication + Admin Role)
|
||||
|
||||
-----
|
||||
|
||||
### 6\. Get All Admin Bookings
|
||||
|
||||
Retrieves a paginated list of all bookings in the system.
|
||||
|
||||
```bash
|
||||
curl -X GET 'http://localhost:8080/api/admin/bookings?limit=50&offset=0' \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "booking-uuid-a",
|
||||
"user_id": "user-uuid-1",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "confirmed",
|
||||
"updated_at": "2025-10-21T10:45:00Z"
|
||||
},
|
||||
{
|
||||
"id": "booking-uuid-b",
|
||||
"user_id": "user-uuid-2",
|
||||
"start_time": "2025-10-26T10:00:00Z",
|
||||
"status": "pending",
|
||||
"updated_at": "2025-10-22T09:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 7\. Search Admin Bookings
|
||||
|
||||
Retrieves a filtered list of bookings based on query parameters.
|
||||
|
||||
```bash
|
||||
# Search for all 'pending' bookings for a specific user ID
|
||||
curl -X GET 'http://localhost:8080/api/admin/bookings/search?status=pending&user_id=user-uuid-1&start_date=2025-10-01' \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
*Same list format as 'Get All Admin Bookings'*
|
||||
|
||||
-----
|
||||
|
||||
### 8\. Get Admin Bookings by User ID
|
||||
|
||||
Retrieves all bookings for a single, specified user.
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/admin/bookings/user/user-uuid \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
*Same list format as 'Get All Admin Bookings'*
|
||||
|
||||
-----
|
||||
|
||||
### 9\. Get Admin Booking by ID
|
||||
|
||||
Retrieves a specific booking including all service and payment details.
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/admin/bookings/booking-uuid \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
*Full booking object, including `services` and `payments` arrays.*
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "confirmed",
|
||||
"notes": "Please use the side entrance",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:45:00Z",
|
||||
"created_by": "admin-uuid",
|
||||
"services": [ ... ],
|
||||
"payments": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 10\. Get Admin Booking Summary
|
||||
|
||||
Retrieves the booking, user details, services, payments, and financial totals in a single, comprehensive response.
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/admin/bookings/booking-uuid/summary \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"booking": {
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "confirmed",
|
||||
"notes": "Please use the side entrance",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:45:00Z",
|
||||
"created_by": "admin-uuid"
|
||||
},
|
||||
"user": {
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email": "john.doe@example.com",
|
||||
"account_role": "client",
|
||||
"loyalty_stamps": 5
|
||||
// ... other user fields
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"service_name": "Haircut",
|
||||
"base_price": 25.00,
|
||||
"override_price": 20.00
|
||||
// ... other service fields
|
||||
}
|
||||
],
|
||||
"payments": [
|
||||
{
|
||||
"id": "payment-uuid",
|
||||
"payment_type": "deposit",
|
||||
"status": "completed",
|
||||
"amount": 10.00,
|
||||
"vat_rate": 20.0
|
||||
// ... other payment fields
|
||||
}
|
||||
],
|
||||
"total_amount": 20.00,
|
||||
"amount_paid": 10.00,
|
||||
"amount_due": 10.00,
|
||||
"duration_minutes": 25
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 11\. Progress Booking Status
|
||||
|
||||
Moves the booking to the next status in its lifecycle (e.g., from `confirmed` to `in_progress`).
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:8080/api/admin/bookings/booking-uuid/progress \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
|
||||
-d '{
|
||||
"status": "in_progress"
|
||||
}'
|
||||
```
|
||||
|
||||
**Valid statuses:** `pending`, `confirmed`, `in_progress`, `completed`, `client_cancelled`, `we_cancelled`, `re-schedule`, `no_show`
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "in_progress",
|
||||
"notes": "Please use the side entrance",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-25T14:05:00Z",
|
||||
"created_by": "admin-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 12\. Confirm Booking (With/Without Overrides)
|
||||
|
||||
Confirms a `pending` booking, optionally applying price/duration overrides and updating notes.
|
||||
|
||||
#### A. Confirm with Overrides
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/confirm \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
|
||||
-d '{
|
||||
"service_overrides": [
|
||||
{
|
||||
"service_id": "service-uuid-1",
|
||||
"override_price": 20.00,
|
||||
"override_duration_minutes": 25
|
||||
}
|
||||
],
|
||||
"notes": "Confirmed by phone. Applied special discount."
|
||||
}'
|
||||
```
|
||||
|
||||
#### B. Confirm with Notes Update Only
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/confirm \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
|
||||
-d '{
|
||||
"notes": "Confirmed via email"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "booking-uuid",
|
||||
"user_id": "user-uuid",
|
||||
"start_time": "2025-10-25T14:00:00Z",
|
||||
"status": "confirmed",
|
||||
"notes": "Confirmed by phone. Applied special discount.",
|
||||
"created_at": "2025-10-21T10:30:00Z",
|
||||
"updated_at": "2025-10-21T10:50:00Z",
|
||||
"created_by": "admin-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 13\. Add Payment to Booking (New Endpoint)
|
||||
|
||||
Adds a new payment record to the specified booking.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/bookings/booking-uuid/payments \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN" \
|
||||
-d '{
|
||||
"payment_type": "final",
|
||||
"payment_method": "cash",
|
||||
"status": "completed",
|
||||
"amount": 15.00,
|
||||
"is_vat_applicable": false,
|
||||
"invoice_number": 1042
|
||||
}'
|
||||
```
|
||||
|
||||
**Valid Payment Types:** `deposit`, `final`
|
||||
**Valid Payment Methods:** `card`, `cash`, `bank_transfer`
|
||||
**Valid Payment Statuses:** `pending`, `completed`, `failed`
|
||||
|
||||
**Response (201 Created):**
|
||||
*Returns the newly created payment object.*
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "new-payment-uuid",
|
||||
"booking_id": "booking-uuid",
|
||||
"payment_type": "final",
|
||||
"payment_method": "cash",
|
||||
"status": "completed",
|
||||
"amount": 15.00,
|
||||
"is_vat_applicable": false,
|
||||
"vat_rate": null,
|
||||
"vat_amount": null,
|
||||
"net_amount": 15.00,
|
||||
"invoice_number": 1042,
|
||||
"created_at": "2025-10-22T12:00:00Z",
|
||||
"updated_at": "2025-10-22T12:00:00Z",
|
||||
"created_by": "admin-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
### 14\. Remove Payment from Booking (New Endpoint)
|
||||
|
||||
Deletes a specific payment record associated with a booking.
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8080/api/admin/bookings/booking-uuid/payments/payment-uuid-to-delete \
|
||||
-H "Authorization: Bearer YOUR_ADMIN_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Payment removed successfully",
|
||||
"id": "payment-uuid-to-delete"
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status Code | Example Response | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **400 Bad Request** | `{"error": "Start time is required"}` | Missing or invalid required fields. |
|
||||
| **401 Unauthorized** | `{"error": "Authentication required"}` | Missing or expired JWT token. |
|
||||
| **403 Forbidden** | `{"error": "Admin access required"}` | Attempting to access an admin endpoint without the necessary role. |
|
||||
| **404 Not Found** | `{"error": "Booking not found or access denied"}` | Resource does not exist or user/admin does not have permission to view it. |
|
||||
| **500 Internal Server Error** | `{"error": "Internal server error"}` | Unhandled server error. |
|
||||
|
||||
-----
|
||||
|
||||
## Notes
|
||||
|
||||
1. Replace `YOUR_JWT_TOKEN` with a standard user's JWT token.
|
||||
2. Replace `YOUR_ADMIN_JWT_TOKEN` with an admin user's JWT token.
|
||||
3. Replace UUIDs (`booking-uuid`, `service-uuid-1`, etc.) with actual IDs from your database.
|
||||
4. All timestamps should be in **RFC3339 format (ISO 8601)**, e.g., `"2025-10-25T14:00:00Z"`.
|
||||
5. Start times must be in the future when creating or editing bookings.
|
||||
6. Bookings can only be **hard-deleted** (Endpoint 5A) if they have no associated payments. Otherwise, a `reason` must be supplied to cancel the booking (Endpoint 5B).
|
||||
7. The **Confirm Booking** endpoint (12) only works on bookings with status `pending`.
|
||||
@@ -0,0 +1,295 @@
|
||||
|
||||
## ✅ / ⏳ Project Checklist
|
||||
|
||||
### Backend (Go / Chi / Postgres)
|
||||
|
||||
- [x] JWT authentication (login, refresh, role verification)
|
||||
- [x] User registration with input validation
|
||||
- [x] Password hashing with bcrypt
|
||||
- [x] Middleware for auth/roles
|
||||
- [x] DB connection pooling
|
||||
- [ ] Booking endpoints
|
||||
- [ ] Admin endpoints
|
||||
- [ ] Unit tests
|
||||
- [ ] CI/CD
|
||||
|
||||
### Frontend (SvelteKit / Tailwind / shadcn)
|
||||
|
||||
- [x] Core pages (Home, Prices, Contact, Book)
|
||||
- [x] Booking wizard prototype
|
||||
- [x] Calendar/time slot selector
|
||||
- [ ] API integration for booking flow
|
||||
- [ ] Payments (Stripe/Square)
|
||||
- [ ] Auth screens
|
||||
- [ ] Admin dashboard
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- [x] `.env` config
|
||||
- [x] Docker Compose full stack
|
||||
- [ ] nginx reverse proxy
|
||||
- [ ] Monitoring/logging
|
||||
- [ ] CI/CD pipeline
|
||||
|
||||
### Integrations
|
||||
|
||||
- [x] CardDAV sync for contacts
|
||||
- [ ] Email/SMS reminders
|
||||
- [ ] Payment provider
|
||||
- [ ] Loyalty tracking frontend
|
||||
|
||||
---
|
||||
|
||||
## 📐 Current Architecture
|
||||
|
||||
### Overview Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User([Customer])
|
||||
Admin([Admin])
|
||||
CardReader[Square Card Reader<br/>Physical Terminal]
|
||||
|
||||
subgraph CF[Cloudflare Protection]
|
||||
CDN[CDN / DDoS Protection]
|
||||
end
|
||||
|
||||
subgraph Lightsail[AWS Lightsail Instance]
|
||||
subgraph NGINX[Nginx Reverse Proxy]
|
||||
Proxy[Port 443/80]
|
||||
end
|
||||
|
||||
subgraph Frontend[SvelteKit Frontend]
|
||||
UI[Booking UI / Prices / Contact]
|
||||
AdminUI[Admin Dashboard]
|
||||
APIProxy[API Proxy Routes]
|
||||
end
|
||||
|
||||
subgraph Backend[Go + Chi Backend]
|
||||
Router[chi Router]
|
||||
Auth[JWT Middleware]
|
||||
Handlers[API Handlers]
|
||||
end
|
||||
|
||||
subgraph Database[PostgreSQL]
|
||||
DB[(Users / Bookings<br/>Transactions)]
|
||||
end
|
||||
|
||||
subgraph DAV[SabreDAV Server]
|
||||
CardDAV[(vCard Contacts)]
|
||||
CalDAV[(Calendar Events)]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph External[External Services]
|
||||
Gmail[Gmail SMTP<br/>smtp.gmail.com:587]
|
||||
SquareAPI[Square API<br/>Online Payments]
|
||||
end
|
||||
|
||||
%% Connections
|
||||
User -->|HTTPS| CF
|
||||
Admin -->|HTTPS| CF
|
||||
CF --> Proxy
|
||||
|
||||
Proxy --> UI
|
||||
Proxy --> AdminUI
|
||||
Proxy --> APIProxy
|
||||
|
||||
UI --> APIProxy
|
||||
AdminUI --> APIProxy
|
||||
APIProxy --> Router
|
||||
|
||||
Router --> Auth
|
||||
Auth --> Handlers
|
||||
|
||||
Handlers --> DB
|
||||
Handlers --> CardDAV
|
||||
Handlers --> CalDAV
|
||||
Handlers -->|Send Emails| Gmail
|
||||
Handlers -->|Process Payments| SquareAPI
|
||||
|
||||
Admin -.->|Sync Contacts/Calendar| DAV
|
||||
|
||||
CardReader -->|Transaction Data| SquareAPI
|
||||
Handlers -.->|Poll Transactions| SquareAPI
|
||||
|
||||
%% Force External Services to appear below
|
||||
Lightsail --> External
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Backend Implementation
|
||||
|
||||
### JWT Auth
|
||||
|
||||
JWT uses **HS256** algorithm with 30-day expiry. Implemented via `go-chi/jwtauth`.
|
||||
|
||||
**Code:**
|
||||
|
||||
```go
|
||||
// auth/jwt.go
|
||||
var TokenAuth *jwtauth.JWTAuth
|
||||
|
||||
func InitJWT(secret string) {
|
||||
TokenAuth = jwtauth.New("HS256", []byte(secret), nil)
|
||||
}
|
||||
|
||||
func GenerateToken(userID string, role string) (string, error) {
|
||||
_, token, err := TokenAuth.Encode(map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"role": role,
|
||||
"exp": time.Now().Add(30 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
return token, err
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
Validates JWT and attaches user context. Supports role restrictions.
|
||||
|
||||
```go
|
||||
// mw/auth.go
|
||||
func RequireAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
http.Error(w, "missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
userID, role, err := auth.VerifyToken(token, r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, UserRoleKey, role)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Registration Flow
|
||||
|
||||
The registration process:
|
||||
|
||||
- Validates names, email, phone, DOB
|
||||
- Rejects users under 16 years old
|
||||
- Hashes password with bcrypt
|
||||
- Saves user profile and creates vCard in CardDAV
|
||||
|
||||
```go
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
tx, _ := db.DB.Begin(r.Context())
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
_, err := tx.Exec(r.Context(),
|
||||
`INSERT INTO users (first_name, last_name, email, phone, dob, password_hash)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)`,
|
||||
req.FirstName, req.LastName, req.Email, req.Phone, dob, string(hash))
|
||||
```
|
||||
|
||||
### CardDAV Integration
|
||||
|
||||
Each registered user generates a `.vcf` file in SabreDAV, ensuring external calendar and contact apps stay in sync.
|
||||
|
||||
```go
|
||||
func saveToCardDAV(userID, firstName, lastName, email, phone, dob string) error {
|
||||
vcard := createVCard(userID, firstName, lastName, email, phone, dob)
|
||||
url := fmt.Sprintf("http://nginx/dav/addressbooks/principals/default/default/%s.vcf", userID)
|
||||
req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(vcard))
|
||||
req.SetBasicAuth("admin", "admin")
|
||||
_, err := http.DefaultClient.Do(req)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Frontend Implementation
|
||||
|
||||
### API Proxy
|
||||
|
||||
Prevents CORS issues by proxying all API calls through SvelteKit.
|
||||
|
||||
```ts
|
||||
// routes/api/[...path]/+server.ts
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
|
||||
|
||||
async function proxyRequest(request: Request, path: string) {
|
||||
const url = `${BACKEND_URL}/api/${path}`;
|
||||
const backendRes = await fetch(url, {
|
||||
method: request.method,
|
||||
headers: request.headers
|
||||
});
|
||||
return new Response(backendRes.body, {
|
||||
status: backendRes.status,
|
||||
headers: backendRes.headers
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Booking Flow
|
||||
|
||||
**Step 1**: Select services
|
||||
**Step 2**: Choose date/time
|
||||
**Step 3**: Enter details
|
||||
**Step 4**: Payment (TODO)
|
||||
|
||||
**Calendar Component:**
|
||||
|
||||
```svelte
|
||||
<Calendar
|
||||
type="single"
|
||||
bind:value={selectedDate}
|
||||
isDateUnavailable={(date) => bookedDates.some(d => d.compare(date) === 0)}
|
||||
/>
|
||||
```
|
||||
|
||||
**Time Slot Generator:**
|
||||
|
||||
```ts
|
||||
function generateTimeSlots(duration: number) {
|
||||
const slots = []
|
||||
for (let hour = 9; hour < 17; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 15) {
|
||||
slots.push(`${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}`);
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
### Bookings
|
||||
|
||||
- Add `bookings` table: user_id, service, date, time, status
|
||||
- Backend: `/api/bookings` (create, list, cancel)
|
||||
- Frontend: wire booking wizard → backend
|
||||
|
||||
### Payments
|
||||
|
||||
- Integrate Stripe or Square SDK
|
||||
- Secure checkout at Step 4
|
||||
|
||||
### Admin Dashboard
|
||||
|
||||
- View/manage bookings
|
||||
- User management
|
||||
- Loyalty overview
|
||||
|
||||
### Security
|
||||
|
||||
- Move JWT secret → env
|
||||
- HTTPS everywhere
|
||||
- Harden API roles
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user