booking modal part 1
This commit is contained in:
+1063
-206
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -23,15 +23,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// 1. Read the environment variable
|
|
||||||
jwtSecret := os.Getenv("JWT_SECRET_KEY")
|
jwtSecret := os.Getenv("JWT_SECRET_KEY")
|
||||||
|
|
||||||
// 2. Add a check to ensure the secret is set
|
|
||||||
if jwtSecret == "" {
|
if jwtSecret == "" {
|
||||||
log.Fatal("FATAL: JWT_SECRET_KEY environment variable not set. Application cannot start.")
|
log.Fatal("FATAL: JWT_SECRET_KEY environment variable not set. Application cannot start.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Use the environment variable for initialization
|
|
||||||
auth.InitJWT(jwtSecret)
|
auth.InitJWT(jwtSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +104,7 @@ func main() {
|
|||||||
|
|
||||||
// Booking routes for authenticated users
|
// Booking routes for authenticated users
|
||||||
r.Route("/bookings", func(r chi.Router) {
|
r.Route("/bookings", func(r chi.Router) {
|
||||||
|
r.Get("/", bookings.GetAllUserBookingsHandler)
|
||||||
r.Post("/", bookings.CreateBookingHandler)
|
r.Post("/", bookings.CreateBookingHandler)
|
||||||
r.Get("/{id}", bookings.GetBookingHandler)
|
r.Get("/{id}", bookings.GetBookingHandler)
|
||||||
r.Put("/{id}", bookings.EditBookingHandler)
|
r.Put("/{id}", bookings.EditBookingHandler)
|
||||||
@@ -129,6 +125,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/admin/bookings", func(r chi.Router) {
|
r.Route("/admin/bookings", func(r chi.Router) {
|
||||||
|
r.Get("/", bookings.GetAllAdminBookingsHandler)
|
||||||
|
r.Get("/search", bookings.SearchAdminBookingsHandler)
|
||||||
|
r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler)
|
||||||
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
r.Get("/{id}", bookings.GetAdminBookingHandler)
|
||||||
r.Get("/{id}/summary", bookings.GetAdminBookingSummaryHandler)
|
r.Get("/{id}/summary", bookings.GetAdminBookingSummaryHandler)
|
||||||
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
r.Put("/{id}/progress", bookings.ProgressBookingHandler)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
} else {
|
} else {
|
||||||
toasterPosition = 'top-center';
|
toasterPosition = 'top-center';
|
||||||
}
|
}
|
||||||
console.log('Toaster position:', toasterPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -463,17 +463,7 @@
|
|||||||
loyalty_stamps?: number;
|
loyalty_stamps?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Booking = {
|
|
||||||
id: string;
|
|
||||||
user_id?: string;
|
|
||||||
start_time: string;
|
|
||||||
status: 'Confirmed' | 'Completed' | 'Cancelled';
|
|
||||||
created_at: string;
|
|
||||||
services?: { id: string; name: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
let userQuery = $state('');
|
let userQuery = $state('');
|
||||||
let bookingQuery = $state('');
|
|
||||||
|
|
||||||
// DEMO DATA: Users
|
// DEMO DATA: Users
|
||||||
let users = $state<User[]>([
|
let users = $state<User[]>([
|
||||||
@@ -499,43 +489,171 @@
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// DEMO DATA: Bookings
|
type Booking = {
|
||||||
let bookings = $state<Booking[]>([
|
id: string;
|
||||||
{
|
user_id: string;
|
||||||
id: 'book1',
|
start_time: string;
|
||||||
user_id: 'user1',
|
status: 'Confirmed' | 'Completed' | 'Cancelled' | 'Pending' | 'In Progress';
|
||||||
start_time: '2025-10-15T10:00:00Z',
|
notes?: string;
|
||||||
status: 'Confirmed',
|
created_at: string;
|
||||||
created_at: '2025-10-01T09:00:00Z',
|
services?: { id: string; name: string }[];
|
||||||
services: [{ id: 'svc1', name: 'Haircut' }]
|
user?: {
|
||||||
},
|
full_name?: string;
|
||||||
{
|
email?: string;
|
||||||
id: 'book2',
|
phone?: string;
|
||||||
user_id: 'user2',
|
};
|
||||||
start_time: '2025-10-14T15:00:00Z',
|
};
|
||||||
status: 'Completed',
|
|
||||||
created_at: '2025-09-28T12:00:00Z',
|
|
||||||
services: [
|
|
||||||
{ id: 'svc2', name: 'Color' },
|
|
||||||
{ id: 'svc1', name: 'Haircut' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'book3',
|
|
||||||
user_id: 'user1',
|
|
||||||
start_time: '2025-10-22T11:00:00Z',
|
|
||||||
status: 'Confirmed',
|
|
||||||
created_at: '2025-10-10T12:00:00Z',
|
|
||||||
services: [{ id: 'svc3', name: 'Blowdry' }]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
let bookings = $state<Booking[]>([]);
|
||||||
|
let bookingQuery = $state('');
|
||||||
let loadingSearch = $state(false);
|
let loadingSearch = $state(false);
|
||||||
let selectedUser = $state<User | null>(null);
|
|
||||||
let selectedBooking = $state<Booking | null>(null);
|
let selectedBooking = $state<Booking | null>(null);
|
||||||
|
let showBookingModal = $state(false);
|
||||||
|
|
||||||
|
// Fetch bookings from API
|
||||||
|
async function fetchBookings() {
|
||||||
|
if (pageState !== 'authorized') return;
|
||||||
|
loadingSearch = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/bookings', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.bookings.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bookings = data.bookings.map((b: any) => ({
|
||||||
|
id: b.id,
|
||||||
|
user_id: b.user_id,
|
||||||
|
start_time: b.start_time,
|
||||||
|
status: b.status,
|
||||||
|
notes: b.notes,
|
||||||
|
created_at: b.created_at,
|
||||||
|
services: b.services?.map((s: any) => ({
|
||||||
|
id: s.service_id,
|
||||||
|
name: s.service_name || 'Unknown Service'
|
||||||
|
})),
|
||||||
|
user: {
|
||||||
|
full_name: b.user?.full_name,
|
||||||
|
email: b.user?.email,
|
||||||
|
phone: b.user?.phone
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
console.log(bookings);
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
toast.error('Failed to load bookings: ' + text);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching bookings:', err);
|
||||||
|
toast.error('Network error loading bookings');
|
||||||
|
} finally {
|
||||||
|
loadingSearch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search bookings via API
|
||||||
|
async function searchBookings() {
|
||||||
|
if (pageState !== 'authorized') return;
|
||||||
|
loadingSearch = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/admin/bookings/search?q=${encodeURIComponent(bookingQuery)}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
bookings = data.bookings.map((b: any) => ({
|
||||||
|
id: b.booking.id,
|
||||||
|
user_id: b.booking.user_id,
|
||||||
|
start_time: b.booking.start_time,
|
||||||
|
status: b.booking.status,
|
||||||
|
notes: b.booking.notes,
|
||||||
|
created_at: b.booking.created_at,
|
||||||
|
services: b.services?.map((s: any) => ({
|
||||||
|
id: s.service_id,
|
||||||
|
name: s.service_name || 'Unknown Service'
|
||||||
|
})),
|
||||||
|
user: {
|
||||||
|
full_name: b.user?.full_name,
|
||||||
|
email: b.user?.email,
|
||||||
|
phone: b.user?.phone
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
toast.error('Failed to search bookings: ' + text);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error searching bookings:', err);
|
||||||
|
toast.error('Network error searching bookings');
|
||||||
|
} finally {
|
||||||
|
loadingSearch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open booking modal with full details
|
||||||
|
async function openBookingModal(bookingId: string) {
|
||||||
|
if (pageState !== 'authorized') return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/bookings/${bookingId}/summary`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${authStore.currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
selectedBooking = {
|
||||||
|
id: data.booking.id,
|
||||||
|
user_id: data.booking.user_id,
|
||||||
|
start_time: data.booking.start_time,
|
||||||
|
status: data.booking.status,
|
||||||
|
notes: data.booking.notes,
|
||||||
|
created_at: data.booking.created_at,
|
||||||
|
services: data.services?.map((s: any) => ({
|
||||||
|
id: s.service_id,
|
||||||
|
name: s.service_name || 'Unknown Service'
|
||||||
|
})),
|
||||||
|
user: {
|
||||||
|
full_name: data.user?.full_name,
|
||||||
|
email: data.user?.email,
|
||||||
|
phone: data.user?.phone
|
||||||
|
}
|
||||||
|
};
|
||||||
|
showBookingModal = true;
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
toast.error('Failed to load booking details: ' + text);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching booking details:', err);
|
||||||
|
toast.error('Network error loading booking details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch bookings on page load
|
||||||
|
$effect(() => {
|
||||||
|
if (pageState === 'authorized') {
|
||||||
|
fetchBookings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedUser = $state<User | null>(null);
|
||||||
let bookingUserHistory = $state<Booking[]>([]);
|
let bookingUserHistory = $state<Booking[]>([]);
|
||||||
let showUserModal = $state(false);
|
let showUserModal = $state(false);
|
||||||
let showBookingModal = $state(false);
|
|
||||||
|
|
||||||
// Uses DEMO data for search
|
// Uses DEMO data for search
|
||||||
async function searchUsers() {
|
async function searchUsers() {
|
||||||
@@ -563,23 +681,6 @@
|
|||||||
loadingSearch = false;
|
loadingSearch = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uses DEMO data for search
|
|
||||||
async function searchBookings() {
|
|
||||||
loadingSearch = true;
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
const query = bookingQuery.toLowerCase();
|
|
||||||
bookings = bookings.filter(
|
|
||||||
(b) =>
|
|
||||||
b.id.includes(query) ||
|
|
||||||
b.services?.some((s) => s.name.toLowerCase().includes(query)) ||
|
|
||||||
users
|
|
||||||
.find((u) => u.id === b.user_id)
|
|
||||||
?.fn?.toLowerCase()
|
|
||||||
.includes(query)
|
|
||||||
);
|
|
||||||
loadingSearch = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openUserModal(userId: string) {
|
async function openUserModal(userId: string) {
|
||||||
selectedUser = users.find((u) => u.id === userId) || null;
|
selectedUser = users.find((u) => u.id === userId) || null;
|
||||||
if (!selectedUser) return;
|
if (!selectedUser) return;
|
||||||
@@ -592,13 +693,6 @@
|
|||||||
showUserModal = true;
|
showUserModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openBookingModal(bookingId: string) {
|
|
||||||
selectedBooking = bookings.find((b) => b.id === bookingId) || null;
|
|
||||||
if (!selectedBooking) return;
|
|
||||||
|
|
||||||
showBookingModal = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============== Helpers ===============
|
// =============== Helpers ===============
|
||||||
function weekdayLabel(i: number) {
|
function weekdayLabel(i: number) {
|
||||||
return dayNames[i];
|
return dayNames[i];
|
||||||
@@ -1330,27 +1424,40 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Booking ID or user"
|
placeholder="Search by name, email, phone, or service"
|
||||||
bind:value={bookingQuery}
|
bind:value={bookingQuery}
|
||||||
onkeyup={(e) => {
|
onkeyup={(e) => {
|
||||||
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
if ((e as KeyboardEvent).key === 'Enter') searchBookings();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onclick={searchBookings} disabled={loadingSearch}>Search</Button>
|
<Button onclick={searchBookings} disabled={loadingSearch}>
|
||||||
|
{loadingSearch ? 'Searching...' : 'Search'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
<div class="mt-3 max-h-60 space-y-2 overflow-y-auto">
|
||||||
{#each bookings as b}
|
{#if loadingSearch}
|
||||||
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
<div class="flex items-center justify-center p-4">
|
||||||
<div>
|
<Skeleton class="h-4 w-32" />
|
||||||
<div class="font-medium">{new Date(b.start_time).toLocaleString()}</div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{b.status} • {b.services?.map((s) => s.name).join(', ')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{:else if bookings.length === 0}
|
||||||
|
<div class="text-center text-sm text-gray-500">No bookings found.</div>
|
||||||
|
{:else}
|
||||||
|
{#each bookings as b}
|
||||||
|
<div class="flex items-center justify-between rounded bg-gray-50 p-2">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">
|
||||||
|
{new Date(b.start_time).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{b.status} • {b.user?.full_name || 'Unknown User'} • {b.services
|
||||||
|
?.map((s) => s.name)
|
||||||
|
.join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onclick={() => openBookingModal(b.id)}>View</Button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
@@ -2360,6 +2467,67 @@
|
|||||||
</Modal.Root>
|
</Modal.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedBooking}
|
||||||
|
<Modal.Root bind:open={showBookingModal}>
|
||||||
|
<Modal.Content class="max-h-[90vh] max-w-sm overflow-y-auto md:max-w-lg">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title class="text-lg font-semibold">
|
||||||
|
Booking: {selectedBooking.id}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<!-- Main Booking Details Grid -->
|
||||||
|
<div class="grid gap-4 px-4 pb-4 md:grid-cols-2">
|
||||||
|
<!-- Column 1: Customer Details -->
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-500">Customer</div>
|
||||||
|
<div class="font-medium">
|
||||||
|
{selectedBooking.user?.full_name || 'Unknown User'}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500">Email</div>
|
||||||
|
<div class="font-medium">{selectedBooking.user?.email || '—'}</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500">Phone</div>
|
||||||
|
<div class="font-medium">{selectedBooking.user?.phone || '—'}</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500">Status</div>
|
||||||
|
<div class="font-medium">{selectedBooking.status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 2: Appointment Details -->
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-500">Scheduled</div>
|
||||||
|
<div class="font-medium">
|
||||||
|
{new Date(selectedBooking.start_time).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-500">Services</div>
|
||||||
|
<div class="mb-4 font-medium">
|
||||||
|
{selectedBooking.services?.map((s) => s.name).join(', ') || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes Section - now full width and visually emphasized -->
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="mb-1 text-sm text-gray-500">Customer Notes</div>
|
||||||
|
{#if selectedBooking.notes && selectedBooking.notes.length > 0}
|
||||||
|
<!-- Applied amber styling to the actual notes content -->
|
||||||
|
<div
|
||||||
|
class="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm font-medium text-amber-900"
|
||||||
|
>
|
||||||
|
{selectedBooking.notes || 'No customer notes provided.'}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="mb-4 font-medium">—</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- END NOTES SECTION -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal.Footer class="flex items-center justify-end gap-2">
|
||||||
|
<Button onclick={() => (showBookingModal = false)}>Close</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal.Root>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Booking Modal -->
|
<!-- Booking Modal -->
|
||||||
{#if showServiceModal}
|
{#if showServiceModal}
|
||||||
<!-- Add Service Modal -->
|
<!-- Add Service Modal -->
|
||||||
|
|||||||
+136
-22
@@ -57,6 +57,8 @@ cat > /tmp/seed_data.sh << 'EOF'
|
|||||||
|
|
||||||
ADMIN_EMAIL="admin@example.com"
|
ADMIN_EMAIL="admin@example.com"
|
||||||
ADMIN_PASS="password"
|
ADMIN_PASS="password"
|
||||||
|
USER_EMAIL="user@example.com"
|
||||||
|
USER_PASS="password"
|
||||||
BASE_URL="http://localhost:8080/api"
|
BASE_URL="http://localhost:8080/api"
|
||||||
|
|
||||||
echo "⏳ Waiting for backend to be ready..."
|
echo "⏳ Waiting for backend to be ready..."
|
||||||
@@ -87,32 +89,60 @@ else
|
|||||||
echo "Response: $RESPONSE_BODY"
|
echo "Response: $RESPONSE_BODY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "2️⃣ Upgrading to Admin Role via psql..."
|
echo "2️⃣ Registering Regular User: $USER_EMAIL"
|
||||||
|
USER_REGISTER_JSON='{"firstName":"Regular","lastName":"User","email":"'$USER_EMAIL'","password":"'$USER_PASS'","phone":"+447000000001","dateOfBirth":"1990-05-15","agreedToPolicy":true}'
|
||||||
|
USER_REGISTER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST -H 'Content-Type: application/json' -d "$USER_REGISTER_JSON" $BASE_URL/register)
|
||||||
|
HTTP_CODE=$(echo "$USER_REGISTER_RESPONSE" | tail -n1)
|
||||||
|
RESPONSE_BODY=$(echo "$USER_REGISTER_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ]; then
|
||||||
|
echo '✅ User registration successful.'
|
||||||
|
else
|
||||||
|
echo "❌ User registration failed with HTTP $HTTP_CODE"
|
||||||
|
echo "Response: $RESPONSE_BODY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "3️⃣ Upgrading Admin Role via psql..."
|
||||||
docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET account_role = 'admin' WHERE email = '$ADMIN_EMAIL'"
|
docker exec postgres psql -U myuser -d mydb -c "UPDATE users SET account_role = 'admin' WHERE email = '$ADMIN_EMAIL'"
|
||||||
|
|
||||||
echo "⏳ Waiting 1 seconds for role update to propagate..."
|
echo "⏳ Waiting 1 seconds for role update to propagate..."
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
echo "3️⃣ Logging in to get JWT token..."
|
echo "4️⃣ Logging in as Admin to get JWT token..."
|
||||||
LOGIN_JSON='{"email":"'$ADMIN_EMAIL'","password":"'$ADMIN_PASS'"}'
|
LOGIN_JSON='{"email":"'$ADMIN_EMAIL'","password":"'$ADMIN_PASS'"}'
|
||||||
LOGIN_RESPONSE=$(curl -s -X POST -H 'Content-Type: application/json' -d "$LOGIN_JSON" $BASE_URL/login)
|
LOGIN_RESPONSE=$(curl -s -X POST -H 'Content-Type: application/json' -d "$LOGIN_JSON" $BASE_URL/login)
|
||||||
|
|
||||||
# Extract token without jq (using grep and sed)
|
# Extract token without jq (using grep and sed)
|
||||||
TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"token":"[^"]*' | sed 's/"token":"//')
|
ADMIN_TOKEN=$(echo "$LOGIN_RESPONSE" | grep -o '"token":"[^"]*' | sed 's/"token":"//')
|
||||||
|
|
||||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
if [ -z "$ADMIN_TOKEN" ] || [ "$ADMIN_TOKEN" = "null" ]; then
|
||||||
echo '❌ Login failed. Cannot proceed with service creation.'
|
echo '❌ Admin login failed. Cannot proceed with service creation.'
|
||||||
echo "Login response: $LOGIN_RESPONSE"
|
echo "Login response: $LOGIN_RESPONSE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo '✅ Login successful. Token obtained.'
|
echo '✅ Admin login successful. Token obtained.'
|
||||||
echo "Token (first 30 chars): ${TOKEN:0:30}..."
|
echo "Admin Token (first 30 chars): ${ADMIN_TOKEN:0:30}..."
|
||||||
|
|
||||||
|
echo "5️⃣ Logging in as User to get JWT token..."
|
||||||
|
USER_LOGIN_JSON='{"email":"'$USER_EMAIL'","password":"'$USER_PASS'"}'
|
||||||
|
USER_LOGIN_RESPONSE=$(curl -s -X POST -H 'Content-Type: application/json' -d "$USER_LOGIN_JSON" $BASE_URL/login)
|
||||||
|
|
||||||
|
# Extract token without jq (using grep and sed)
|
||||||
|
USER_TOKEN=$(echo "$USER_LOGIN_RESPONSE" | grep -o '"token":"[^"]*' | sed 's/"token":"//')
|
||||||
|
|
||||||
|
if [ -z "$USER_TOKEN" ] || [ "$USER_TOKEN" = "null" ]; then
|
||||||
|
echo '❌ User login failed. Cannot proceed with booking creation.'
|
||||||
|
echo "Login response: $USER_LOGIN_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo '✅ User login successful. Token obtained.'
|
||||||
|
echo "User Token (first 30 chars): ${USER_TOKEN:0:30}..."
|
||||||
|
|
||||||
# Add extra delay to ensure token is fully processed
|
# Add extra delay to ensure token is fully processed
|
||||||
echo "⏳ Waiting 1 seconds before creating services..."
|
echo "⏳ Waiting 1 seconds before creating services..."
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
echo '4️⃣ Creating 6 Nail Bar Services...'
|
echo 6️⃣ Creating 6 Nail Bar Services...
|
||||||
|
|
||||||
# Updated services with correct field names based on CreateServiceRequest struct
|
# Updated services with correct field names based on CreateServiceRequest struct
|
||||||
# Note: description is optional (omitempty), all other fields are required
|
# Note: description is optional (omitempty), all other fields are required
|
||||||
@@ -125,6 +155,7 @@ SERVICES=(
|
|||||||
'{"name":"Nail Art Add-on","description":"Custom nail art, per two fingers.","price":5.00,"duration_minutes":15,"patch_test_duration_hours":0,"minimum_age_required":0}'
|
'{"name":"Nail Art Add-on","description":"Custom nail art, per two fingers.","price":5.00,"duration_minutes":15,"patch_test_duration_hours":0,"minimum_age_required":0}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERVICE_IDS=()
|
||||||
SUCCESS_COUNT=0
|
SUCCESS_COUNT=0
|
||||||
FAIL_COUNT=0
|
FAIL_COUNT=0
|
||||||
for SERVICE_JSON in "${SERVICES[@]}"; do
|
for SERVICE_JSON in "${SERVICES[@]}"; do
|
||||||
@@ -134,26 +165,27 @@ for SERVICE_JSON in "${SERVICES[@]}"; do
|
|||||||
echo "Creating: $SERVICE_NAME"
|
echo "Creating: $SERVICE_NAME"
|
||||||
echo "Request JSON: $SERVICE_JSON"
|
echo "Request JSON: $SERVICE_JSON"
|
||||||
|
|
||||||
CREATE_RESPONSE=$(curl -v -X POST \
|
CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||||
-d "$SERVICE_JSON" \
|
-d "$SERVICE_JSON" \
|
||||||
"$BASE_URL/admin/services" 2>&1)
|
"$BASE_URL/admin/services")
|
||||||
|
|
||||||
# Extract HTTP code from verbose output
|
HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1)
|
||||||
HTTP_CODE=$(echo "$CREATE_RESPONSE" | grep "< HTTP" | awk '{print $3}')
|
RESPONSE_BODY=$(echo "$CREATE_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
# Get the response body (last few lines after headers)
|
|
||||||
RESPONSE_BODY=$(echo "$CREATE_RESPONSE" | sed -n '/^{/,$p' | grep '^{')
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" = "201" ]; then
|
if [ "$HTTP_CODE" = "201" ]; then
|
||||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||||
echo "✅ Created: $SERVICE_NAME"
|
echo "✅ Created: $SERVICE_NAME"
|
||||||
|
# Extract service ID from response
|
||||||
|
SERVICE_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":"[^"]*' | cut -d'"' -f4)
|
||||||
|
if [ -n "$SERVICE_ID" ]; then
|
||||||
|
SERVICE_IDS+=("$SERVICE_ID")
|
||||||
|
echo " Service ID: $SERVICE_ID"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||||
echo "❌ Failed to create: $SERVICE_NAME (HTTP $HTTP_CODE)"
|
echo "❌ Failed to create: $SERVICE_NAME (HTTP $HTTP_CODE)"
|
||||||
echo "Full curl output:"
|
|
||||||
echo "$CREATE_RESPONSE"
|
|
||||||
if [ -n "$RESPONSE_BODY" ]; then
|
if [ -n "$RESPONSE_BODY" ]; then
|
||||||
echo "Response body: $RESPONSE_BODY"
|
echo "Response body: $RESPONSE_BODY"
|
||||||
fi
|
fi
|
||||||
@@ -166,9 +198,91 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|||||||
echo "✅ Successfully created: $SUCCESS_COUNT services"
|
echo "✅ Successfully created: $SUCCESS_COUNT services"
|
||||||
echo "❌ Failed: $FAIL_COUNT services"
|
echo "❌ Failed: $FAIL_COUNT services"
|
||||||
|
|
||||||
# --- 5️⃣ Create Holiday Exceptional Groups ---
|
# Display collected service IDs
|
||||||
echo ""
|
echo ""
|
||||||
echo "5️⃣ Creating Holiday Exceptional Groups..."
|
echo "Created Service IDs:"
|
||||||
|
for i in "${!SERVICE_IDS[@]}"; do
|
||||||
|
echo " $((i+1)). ${SERVICE_IDS[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- 7️⃣ Create Demo Bookings ---
|
||||||
|
echo ""
|
||||||
|
echo "7️⃣ Creating 6 Demo Bookings..."
|
||||||
|
|
||||||
|
# Check if we have enough services created
|
||||||
|
if [ ${#SERVICE_IDS[@]} -lt 6 ]; then
|
||||||
|
echo "❌ ERROR: Only ${#SERVICE_IDS[@]} services were created successfully."
|
||||||
|
echo " Need at least 6 services to create demo bookings."
|
||||||
|
echo " Skipping booking creation..."
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "✅ All 6 services available. Proceeding with booking creation..."
|
||||||
|
|
||||||
|
# Function to create a booking
|
||||||
|
create_booking() {
|
||||||
|
local TOKEN=$1
|
||||||
|
local START_TIME=$2
|
||||||
|
local SERVICE_IDS_JSON=$3
|
||||||
|
local NOTES=$4
|
||||||
|
local BOOKING_NAME=$5
|
||||||
|
|
||||||
|
local BOOKING_JSON="{\"start_time\":\"$START_TIME\",\"service_ids\":$SERVICE_IDS_JSON"
|
||||||
|
if [ -n "$NOTES" ]; then
|
||||||
|
BOOKING_JSON="$BOOKING_JSON,\"notes\":\"$NOTES\""
|
||||||
|
fi
|
||||||
|
BOOKING_JSON="$BOOKING_JSON}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Creating: $BOOKING_NAME"
|
||||||
|
echo "Time: $START_TIME"
|
||||||
|
echo "Services: $SERVICE_IDS_JSON"
|
||||||
|
|
||||||
|
CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-d "$BOOKING_JSON" \
|
||||||
|
"$BASE_URL/bookings")
|
||||||
|
|
||||||
|
HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1)
|
||||||
|
RESPONSE_BODY=$(echo "$CREATE_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ]; then
|
||||||
|
echo "✅ Created: $BOOKING_NAME"
|
||||||
|
BOOKING_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":"[^"]*' | cut -d'"' -f4)
|
||||||
|
echo " Booking ID: $BOOKING_ID"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to create: $BOOKING_NAME (HTTP $HTTP_CODE)"
|
||||||
|
if [ -n "$RESPONSE_BODY" ]; then
|
||||||
|
echo "Response body: $RESPONSE_BODY"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Establish the base day (Tomorrow) with a time that guarantees it's in the future.
|
||||||
|
TOMORROW_BASE=$(date -u -d "tomorrow 08:00:00" +%Y-%m-%d)
|
||||||
|
|
||||||
|
# 2. Calculate the date 7 days after the TOMORROW_BASE date
|
||||||
|
NEXT_WEEK_DATE=$(date -u -d "$TOMORROW_BASE +7 days" +%Y-%m-%d)
|
||||||
|
|
||||||
|
# 3. Calculate the date 14 days after the TOMORROW_BASE date
|
||||||
|
WEEK_AFTER_DATE=$(date -u -d "$TOMORROW_BASE +14 days" +%Y-%m-%d)
|
||||||
|
|
||||||
|
|
||||||
|
# Create demo bookings using the full timestamp (using the fixed dates)
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$TOMORROW_BASE 10:00:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[0]}\"]" "" "Classic Manicure - Tomorrow 10:00"
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$TOMORROW_BASE 14:30:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[1]}\",\"${SERVICE_IDS[5]}\"]" "Want French manicure with simple nail art on accent fingers" "Gel Manicure + Nail Art - Tomorrow 14:30"
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$NEXT_WEEK_DATE 11:00:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[2]}\"]" "Special treat for myself" "Luxury Pedicure - Next Week 11:00"
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$NEXT_WEEK_DATE 15:45:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[3]}\"]" "Need quick refresh before event" "Express Mani & Pedi - Next Week 15:45"
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$WEEK_AFTER_DATE 13:15:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[4]}\",\"${SERVICE_IDS[1]}\"]" "Remove old gel and apply new BIAB" "Gel Removal + New Gel - Week After 13:15"
|
||||||
|
create_booking "$USER_TOKEN" "$(date -u -d "$WEEK_AFTER_DATE 16:30:00" +%Y-%m-%dT%H:%M:%SZ)" "[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" "Birthday celebration - want something special!" "Classic + Nail Art - Week After 16:30"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 8️⃣ Create Holiday Exceptional Groups ---
|
||||||
|
echo ""
|
||||||
|
echo "8️⃣ Creating Holiday Exceptional Groups..."
|
||||||
|
|
||||||
# November Break (week of Nov 10-16, 2025) - Closed entirely
|
# November Break (week of Nov 10-16, 2025) - Closed entirely
|
||||||
NOVEMBER_BREAK='{
|
NOVEMBER_BREAK='{
|
||||||
@@ -191,7 +305,7 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|||||||
echo "Creating: November Break"
|
echo "Creating: November Break"
|
||||||
NOV_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
NOV_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||||
-d "$NOVEMBER_BREAK" \
|
-d "$NOVEMBER_BREAK" \
|
||||||
"$BASE_URL/scheduling/exceptional-groups")
|
"$BASE_URL/scheduling/exceptional-groups")
|
||||||
HTTP_CODE=$(echo "$NOV_RESPONSE" | tail -n1)
|
HTTP_CODE=$(echo "$NOV_RESPONSE" | tail -n1)
|
||||||
@@ -227,7 +341,7 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|||||||
echo "Creating: Christmas Holiday Period"
|
echo "Creating: Christmas Holiday Period"
|
||||||
XMAS_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
XMAS_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||||
-d "$CHRISTMAS_BREAK" \
|
-d "$CHRISTMAS_BREAK" \
|
||||||
"$BASE_URL/scheduling/exceptional-groups")
|
"$BASE_URL/scheduling/exceptional-groups")
|
||||||
HTTP_CODE=$(echo "$XMAS_RESPONSE" | tail -n1)
|
HTTP_CODE=$(echo "$XMAS_RESPONSE" | tail -n1)
|
||||||
|
|||||||
Reference in New Issue
Block a user