Added booking approvals
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
package admin
|
|
||||||
@@ -771,6 +771,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// ----------------------------
|
// ----------------------------
|
||||||
serviceRows, err := db.DB.Query(r.Context(), `
|
serviceRows, err := db.DB.Query(r.Context(), `
|
||||||
SELECT
|
SELECT
|
||||||
|
bs.service_id,
|
||||||
s.name,
|
s.name,
|
||||||
coalesce(bs.override_price, s.price) as price,
|
coalesce(bs.override_price, s.price) as price,
|
||||||
coalesce(bs.override_duration_minutes, s.duration_minutes) as duration_minutes
|
coalesce(bs.override_duration_minutes, s.duration_minutes) as duration_minutes
|
||||||
@@ -790,11 +791,12 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
var durationMinutesTotal int
|
var durationMinutesTotal int
|
||||||
|
|
||||||
for serviceRows.Next() {
|
for serviceRows.Next() {
|
||||||
|
var serviceID string
|
||||||
var name string
|
var name string
|
||||||
var price float64
|
var price float64
|
||||||
var durationMinutes int
|
var durationMinutes int
|
||||||
|
|
||||||
if err := serviceRows.Scan(&name, &price, &durationMinutes); err != nil {
|
if err := serviceRows.Scan(&serviceID, &name, &price, &durationMinutes); err != nil {
|
||||||
log.Printf("Failed to scan service for booking %s: %v", bookingID, err)
|
log.Printf("Failed to scan service for booking %s: %v", bookingID, err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -804,6 +806,7 @@ func GetAdminBookingHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
totalAmount += price
|
totalAmount += price
|
||||||
durationMinutesTotal += durationMinutes
|
durationMinutesTotal += durationMinutes
|
||||||
booking.Services = append(booking.Services, BookingService{
|
booking.Services = append(booking.Services, BookingService{
|
||||||
|
ServiceID: serviceID, // now set
|
||||||
ServiceName: &name,
|
ServiceName: &name,
|
||||||
Price: &price,
|
Price: &price,
|
||||||
DurationMinutes: &durationMinutes,
|
DurationMinutes: &durationMinutes,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"crussell/handlers/notifications"
|
"crussell/handlers/notifications"
|
||||||
"crussell/handlers/scheduling"
|
"crussell/handlers/scheduling"
|
||||||
"crussell/handlers/services"
|
"crussell/handlers/services"
|
||||||
|
"crussell/handlers/today"
|
||||||
"crussell/handlers/user"
|
"crussell/handlers/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,6 +141,12 @@ func main() {
|
|||||||
r.Get("/{id}", user.GetAdminUserHandler)
|
r.Get("/{id}", user.GetAdminUserHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Route("/admin/today", func(r chi.Router) {
|
||||||
|
r.Get("/current-next", today.GetCurrentAndNextHandler)
|
||||||
|
r.Get("/appointments", today.GetTodayAppointmentsHandler)
|
||||||
|
r.Get("/pending-approvals", today.GetPendingApprovalsHandler)
|
||||||
|
})
|
||||||
|
|
||||||
// --- Admin Notifications ---
|
// --- Admin Notifications ---
|
||||||
r.Route("/admin/notifications", func(r chi.Router) {
|
r.Route("/admin/notifications", func(r chi.Router) {
|
||||||
// List all unacknowledged notifications
|
// List all unacknowledged notifications
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"svelte-sonner": "^1.0.5",
|
"svelte-sonner": "^1.0.5",
|
||||||
"sveltekit-superforms": "^2.27.1",
|
"sveltekit-superforms": "^2.27.1",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "^3.1.1",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import * as Modal from '$lib/components/ui/dialog';
|
import * as Modal from '$lib/components/ui/dialog';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import ApprovalModal from '$lib/components/admin/ApprovalModal.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -31,6 +32,8 @@
|
|||||||
|
|
||||||
user?: {
|
user?: {
|
||||||
id: string;
|
id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
@@ -80,6 +83,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let selectedBooking = $state<Booking | null>(null);
|
let selectedBooking = $state<Booking | null>(null);
|
||||||
|
let showApprovalModal = $state(false);
|
||||||
|
|
||||||
|
// Calculate total duration from services
|
||||||
|
let totalDuration = $derived(
|
||||||
|
selectedBooking?.services?.reduce((sum, service) => sum + (service.duration_minutes || 0), 0) ||
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
async function fetchBookingDetails() {
|
async function fetchBookingDetails() {
|
||||||
if (!bookingId) return;
|
if (!bookingId) return;
|
||||||
@@ -104,6 +114,8 @@
|
|||||||
user: data.user
|
user: data.user
|
||||||
? {
|
? {
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
|
first_name: data.user.first_name,
|
||||||
|
last_name: data.user.last_name,
|
||||||
full_name: data.user.full_name,
|
full_name: data.user.full_name,
|
||||||
email: data.user.email,
|
email: data.user.email,
|
||||||
phone: data.user.phone,
|
phone: data.user.phone,
|
||||||
@@ -178,28 +190,35 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if selectedBooking}
|
{#if selectedBooking}
|
||||||
<span
|
{#if selectedBooking.status === 'pending'}
|
||||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium
|
<div class="flex items-center gap-2">
|
||||||
{selectedBooking.status === 'pending'
|
<Button
|
||||||
? 'bg-yellow-100 text-yellow-800'
|
onclick={() => (showApprovalModal = true)}
|
||||||
: selectedBooking.status === 'confirmed'
|
class="bg-emerald-600 hover:bg-emerald-700"
|
||||||
? 'bg-emerald-100 text-emerald-800'
|
>
|
||||||
: selectedBooking.status === 'in_progress'
|
<svg
|
||||||
? 'bg-blue-100 text-blue-800'
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
: selectedBooking.status === 'completed'
|
class="mr-2 h-4 w-4"
|
||||||
? 'bg-green-100 text-green-800'
|
viewBox="0 0 20 20"
|
||||||
: selectedBooking.status === 'client_cancelled'
|
fill="currentColor"
|
||||||
? 'bg-red-100 text-red-800'
|
>
|
||||||
: selectedBooking.status === 'we_cancelled'
|
<path
|
||||||
? 'bg-rose-100 text-rose-800'
|
fill-rule="evenodd"
|
||||||
: selectedBooking.status === 're-schedule'
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
? 'bg-purple-100 text-purple-800'
|
clip-rule="evenodd"
|
||||||
: selectedBooking.status === 'no_show'
|
/>
|
||||||
? 'bg-gray-100 text-gray-800'
|
</svg>
|
||||||
: 'bg-gray-100 text-gray-800'}"
|
Approve Booking
|
||||||
>
|
</Button>
|
||||||
{selectedBooking.status.charAt(0).toUpperCase() + selectedBooking.status.slice(1)}
|
|
||||||
</span>
|
<span
|
||||||
|
class="inline-flex items-center rounded-full bg-yellow-100 px-3 py-1 text-sm
|
||||||
|
font-medium text-yellow-800"
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
@@ -233,7 +252,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-gray-500">Duration</div>
|
<div class="text-xs text-gray-500">Duration</div>
|
||||||
<div class="font-medium">{selectedBooking.duration_minutes} minutes</div>
|
<div class="font-medium">{totalDuration} minutes</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-gray-500">Created</div>
|
<div class="text-xs text-gray-500">Created</div>
|
||||||
@@ -262,6 +281,29 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
|
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
||||||
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
||||||
|
Services
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
{#each selectedBooking.services as service, index (index)}
|
||||||
|
<div class="rounded-md border border-gray-300 bg-white p-3">
|
||||||
|
<div class="font-medium">{service.service_name || '—'}</div>
|
||||||
|
{#if service.service_description}
|
||||||
|
<div class="mt-1 text-sm text-gray-600">{service.service_description}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="mt-2 flex items-center justify-between text-sm">
|
||||||
|
<span class="text-gray-600">{service.duration_minutes} min</span>
|
||||||
|
<span class="font-semibold">£{service.price?.toFixed(2) || '0.00'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Customer Information -->
|
<!-- Customer Information -->
|
||||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
||||||
@@ -311,29 +353,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services -->
|
|
||||||
{#if selectedBooking.services && selectedBooking.services.length > 0}
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
||||||
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
|
||||||
Services
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
{#each selectedBooking.services as service, index (index)}
|
|
||||||
<div class="rounded-md border border-gray-300 bg-white p-3">
|
|
||||||
<div class="font-medium">{service.service_name || '—'}</div>
|
|
||||||
{#if service.service_description}
|
|
||||||
<div class="mt-1 text-sm text-gray-600">{service.service_description}</div>
|
|
||||||
{/if}
|
|
||||||
<div class="mt-2 flex items-center justify-between text-sm">
|
|
||||||
<span class="text-gray-600">{service.duration_minutes} min</span>
|
|
||||||
<span class="font-semibold">£{service.price?.toFixed(2) || '0.00'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Financial Summary -->
|
<!-- Financial Summary -->
|
||||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
<h3 class="mb-3 text-sm font-semibold tracking-wide text-gray-600 uppercase">
|
||||||
@@ -435,3 +454,15 @@
|
|||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal.Root>
|
</Modal.Root>
|
||||||
|
|
||||||
|
<!-- Approval Sub-Modal -->
|
||||||
|
{#if selectedBooking && showApprovalModal}
|
||||||
|
<ApprovalModal
|
||||||
|
bind:open={showApprovalModal}
|
||||||
|
booking={selectedBooking}
|
||||||
|
onApproved={() => {
|
||||||
|
showApprovalModal = false;
|
||||||
|
fetchBookingDetails();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -711,7 +711,7 @@
|
|||||||
<li>• Share your referral code with friends</li>
|
<li>• Share your referral code with friends</li>
|
||||||
<li>• They get 10% off their first booking</li>
|
<li>• They get 10% off their first booking</li>
|
||||||
<li>• You get 10% off your next booking after they claim</li>
|
<li>• You get 10% off your next booking after they claim</li>
|
||||||
<li>• You earn 1 loyalty stamp for each use to keep the savings going</li>
|
<li>• You earn 3 loyalty stamp for each use to keep the savings going</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|||||||
@@ -262,6 +262,26 @@ format_london_time() {
|
|||||||
TZ=Europe/London date -d "$DATE_PART $TIME_PART" +"%Y-%m-%dT%H:%M:%S%:z"
|
TZ=Europe/London date -d "$DATE_PART $TIME_PART" +"%Y-%m-%dT%H:%M:%S%:z"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- TODAY BOOKINGS (4 bookings) ---
|
||||||
|
echo ""
|
||||||
|
echo "Creating 4 bookings for today..."
|
||||||
|
|
||||||
|
TODAY_DATE=$(TZ=Europe/London date +%Y-%m-%d)
|
||||||
|
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$TODAY_DATE" "09:30:00")" \
|
||||||
|
"[\"${SERVICE_IDS[0]}\"]" "" "Today - Classic Manicure 09:30"
|
||||||
|
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$TODAY_DATE" "11:00:00")" \
|
||||||
|
"[\"${SERVICE_IDS[1]}\"]" "" "Today - Gel Manicure 11:00"
|
||||||
|
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$TODAY_DATE" "13:30:00")" \
|
||||||
|
"[\"${SERVICE_IDS[3]}\"]" "Lunch break slot" "Today - Express Mani & Pedi 13:30"
|
||||||
|
|
||||||
|
create_booking "$USER_TOKEN" "$(format_london_time "$TODAY_DATE" "16:00:00")" \
|
||||||
|
"[\"${SERVICE_IDS[0]}\",\"${SERVICE_IDS[5]}\"]" \
|
||||||
|
"End of day treat" "Today - Classic + Nail Art 16:00"
|
||||||
|
|
||||||
|
|
||||||
# --- PAST BOOKINGS (35 bookings from October-November 2025) ---
|
# --- PAST BOOKINGS (35 bookings from October-November 2025) ---
|
||||||
echo ""
|
echo ""
|
||||||
echo "Creating 35 past bookings..."
|
echo "Creating 35 past bookings..."
|
||||||
|
|||||||
Reference in New Issue
Block a user