diff --git a/quests.json b/quests.json index d5fef4a..8a8c964 100644 --- a/quests.json +++ b/quests.json @@ -1,8 +1,8 @@ -{ - "name": "New Project", - "description": "", - "icon": { - "lucide": "square-dashed", - "background": "conic-gradient(from 42deg at 50% 50%, #18181b, #27272a)" - } -} +{ + "description": "", + "icon": { + "background": "conic-gradient(from 42deg at 50% 50%, #ffd60a, #26cd41)", + "lucide": "calendar" + }, + "name": "Beauty Shop Booking Platform" +} \ No newline at end of file diff --git a/src/client/app.tsx b/src/client/app.tsx index de11294..29abc67 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -1,14 +1,115 @@ -function App() { - return ( -
- {/* Replace this placeholder content with your app components */} -
-

- Building your new project... -

-
-
- ); -} - -export default App; +import { useState } from "react"; +import { BookingForm } from "@/client/components/booking-form"; +import { AdminTreatments } from "@/client/components/admin-treatments"; +import { AdminBookings } from "@/client/components/admin-bookings"; +import { InitialDataLoader } from "@/client/components/initial-data-loader"; + +function App() { + const [activeTab, setActiveTab] = useState<"booking" | "admin-treatments" | "admin-bookings">("booking"); + + const tabs = [ + { id: "booking", label: "Book Appointment", icon: "📅" }, + { id: "admin-treatments", label: "Manage Treatments", icon: "💅" }, + { id: "admin-bookings", label: "Manage Bookings", icon: "📋" }, + ] as const; + + return ( +
+ + + {/* Header */} +
+
+
+
+
💅
+
+

Bella Nails Studio

+

Professional Nail Design & Care

+
+
+
+
+
+ + {/* Navigation */} + + + {/* Main Content */} +
+ {activeTab === "booking" && ( +
+
+

+ Book Your Perfect Nail Treatment +

+

+ Experience professional nail care with our expert technicians. + Choose from our wide range of treatments and book your appointment today. +

+
+ +
+ )} + + {activeTab === "admin-treatments" && ( +
+
+

+ Treatment Management +

+

+ Add, edit, and manage your nail treatment services. +

+
+ +
+ )} + + {activeTab === "admin-bookings" && ( +
+
+

+ Booking Management +

+

+ View and manage customer appointments and bookings. +

+
+ +
+ )} +
+ + {/* Footer */} + +
+ ); +} + +export default App; diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx new file mode 100644 index 0000000..7f39d1c --- /dev/null +++ b/src/client/components/admin-bookings.tsx @@ -0,0 +1,213 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { queryClient } from "@/client/rpc-client"; + +export function AdminBookings() { + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + + const { data: bookings } = useQuery( + queryClient.bookings.live.list.experimental_liveOptions() + ); + + const { data: treatments } = useQuery( + queryClient.treatments.live.list.experimental_liveOptions() + ); + + const { mutate: updateBookingStatus } = useMutation( + queryClient.bookings.updateStatus.mutationOptions() + ); + + const getTreatmentName = (treatmentId: string) => { + return treatments?.find(t => t.id === treatmentId)?.name || "Unknown Treatment"; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "pending": return "bg-yellow-100 text-yellow-800"; + case "confirmed": return "bg-green-100 text-green-800"; + case "cancelled": return "bg-red-100 text-red-800"; + case "completed": return "bg-blue-100 text-blue-800"; + default: return "bg-gray-100 text-gray-800"; + } + }; + + const filteredBookings = bookings?.filter(booking => + selectedDate ? booking.appointmentDate === selectedDate : true + ).sort((a, b) => { + if (a.appointmentDate === b.appointmentDate) { + return a.appointmentTime.localeCompare(b.appointmentTime); + } + return a.appointmentDate.localeCompare(b.appointmentDate); + }); + + const upcomingBookings = bookings?.filter(booking => { + const bookingDate = new Date(booking.appointmentDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + return bookingDate >= today && booking.status !== "cancelled"; + }).sort((a, b) => { + if (a.appointmentDate === b.appointmentDate) { + return a.appointmentTime.localeCompare(b.appointmentTime); + } + return a.appointmentDate.localeCompare(b.appointmentDate); + }); + + return ( +
+

Manage Bookings

+ + {/* Quick Stats */} +
+
+
+ {bookings?.filter(b => b.status === "pending").length || 0} +
+
Pending Approval
+
+
+
+ {bookings?.filter(b => b.status === "confirmed").length || 0} +
+
Confirmed
+
+
+
+ {upcomingBookings?.length || 0} +
+
Upcoming
+
+
+
+ {bookings?.length || 0} +
+
Total Bookings
+
+
+ + {/* Date Filter */} +
+
+ + setSelectedDate(e.target.value)} + className="p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + /> + +
+
+ + {/* Bookings Table */} +
+ + + + + + + + + + + + {filteredBookings?.map((booking) => ( + + + + + + + + ))} + +
+ Customer + + Treatment + + Date & Time + + Status + + Actions +
+
+
{booking.customerName}
+
{booking.customerEmail}
+
{booking.customerPhone}
+
+
+
{getTreatmentName(booking.treatmentId)}
+ {booking.notes && ( +
Notes: {booking.notes}
+ )} +
+
{new Date(booking.appointmentDate).toLocaleDateString()}
+
{booking.appointmentTime}
+
+ + {booking.status} + + +
+ {booking.status === "pending" && ( + <> + + + + )} + {booking.status === "confirmed" && ( + <> + + + + )} + {(booking.status === "cancelled" || booking.status === "completed") && ( + + )} +
+
+ + {!filteredBookings?.length && ( +
+ {selectedDate + ? `No bookings found for ${new Date(selectedDate).toLocaleDateString()}` + : "No bookings available." + } +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/client/components/admin-treatments.tsx b/src/client/components/admin-treatments.tsx new file mode 100644 index 0000000..8c03b65 --- /dev/null +++ b/src/client/components/admin-treatments.tsx @@ -0,0 +1,270 @@ +import { useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { queryClient } from "@/client/rpc-client"; + +export function AdminTreatments() { + const [showForm, setShowForm] = useState(false); + const [editingTreatment, setEditingTreatment] = useState(null); + const [formData, setFormData] = useState({ + name: "", + description: "", + duration: 60, + price: 5000, // $50.00 in cents + category: "Manicure", + }); + + const { data: treatments } = useQuery( + queryClient.treatments.live.list.experimental_liveOptions() + ); + + const { mutate: createTreatment, isPending: isCreating } = useMutation( + queryClient.treatments.create.mutationOptions() + ); + + const { mutate: updateTreatment, isPending: isUpdating } = useMutation( + queryClient.treatments.update.mutationOptions() + ); + + const { mutate: deleteTreatment } = useMutation( + queryClient.treatments.remove.mutationOptions() + ); + + const categories = ["Manicure", "Pedicure", "Nail Art", "Extensions", "Other"]; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (editingTreatment) { + updateTreatment({ + id: editingTreatment.id, + ...formData, + }, { + onSuccess: () => { + setEditingTreatment(null); + setShowForm(false); + resetForm(); + } + }); + } else { + createTreatment(formData, { + onSuccess: () => { + setShowForm(false); + resetForm(); + } + }); + } + }; + + const resetForm = () => { + setFormData({ + name: "", + description: "", + duration: 60, + price: 5000, + category: "Manicure", + }); + }; + + const handleEdit = (treatment: any) => { + setEditingTreatment(treatment); + setFormData({ + name: treatment.name, + description: treatment.description, + duration: treatment.duration, + price: treatment.price, + category: treatment.category, + }); + setShowForm(true); + }; + + const handleCancel = () => { + setShowForm(false); + setEditingTreatment(null); + resetForm(); + }; + + return ( +
+
+

Manage Treatments

+ +
+ + {showForm && ( +
+

+ {editingTreatment ? "Edit Treatment" : "Add New Treatment"} +

+ +
+
+
+ + setFormData({...formData, name: e.target.value})} + className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + required + /> +
+
+ + +
+
+ +
+ +