Fix web routing

This commit is contained in:
augmentedpotato
2026-04-02 08:54:24 -06:00
parent 3d6b87a7d2
commit 82935303ba
12 changed files with 407 additions and 252 deletions

View File

@@ -1,5 +1,6 @@
"use client";
import dynamic from "next/dynamic";
import { useState, useEffect, useCallback, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
@@ -68,8 +69,12 @@ function DatePicker({ value, minDate, onChange }) {
}
const cells = [];
for (let i = 0; i < firstDay; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
for (let i = 0; i < firstDay; i++) {
cells.push({ key: `empty-${viewYear}-${viewMonth}-${String(i)}`, day: null });
}
for (let d = 1; d <= daysInMonth; d++) {
cells.push({ key: `day-${viewYear}-${viewMonth}-${String(d)}`, day: d });
}
const s = {
widget: {
@@ -160,12 +165,12 @@ function DatePicker({ value, minDate, onChange }) {
{DAYS.map((d) => (
<span key={d} style={s.dayName}>{d}</span>
))}
{cells.map((day, i) =>
{cells.map(({ key, day }) =>
day === null ? (
<span key={`empty-${i}`} />
<span key={key} />
) : (
<button
key={day}
key={key}
type="button"
style={{
...s.dayBase,
@@ -190,7 +195,7 @@ function DatePicker({ value, minDate, onChange }) {
);
}
export default function AppointmentsPage() {
function AppointmentsPage() {
const { user, token, loading: authLoading } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
@@ -217,6 +222,8 @@ export default function AppointmentsPage() {
const [appointments, setAppointments] = useState([]);
const [loadingAppointments, setLoadingAppointments] = useState(false);
const canBookAppointments = user?.role === "CUSTOMER";
useEffect(() => {
if (!authLoading && !user) {
router.push("/login");
@@ -241,18 +248,20 @@ export default function AppointmentsPage() {
.then((data) => setServices(data.content ?? []))
.catch(() => {});
fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc`)
fetch(`${API_BASE}/api/v1/pets?size=200&sort=petId,asc`)
.then((r) => r.json())
.then((data) => setAllPets(data.content ?? []))
.catch(() => {});
fetch(`${API_BASE}/api/v1/my-pets`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
.catch(() => {});
}, [token]);
if (canBookAppointments) {
fetch(`${API_BASE}/api/v1/my-pets`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
.catch(() => {});
}
}, [token, canBookAppointments]);
useEffect(() => {
if (didPreselectRef.current) {
@@ -366,6 +375,12 @@ export default function AppointmentsPage() {
setError(null);
setSuccess(null);
if (!canBookAppointments) {
setError("Only customer accounts can book appointments from the web app.");
return;
}
if (!user?.customerId) {
setError("Customer account not found. Please contact support.");
@@ -458,149 +473,159 @@ export default function AppointmentsPage() {
</section>
<section className="appt-content">
<form className="appt-form" onSubmit={handleSubmit}>
<h2 className="appt-form-title">New Appointment</h2>
{canBookAppointments ? (
<form className="appt-form" onSubmit={handleSubmit}>
<h2 className="appt-form-title">New Appointment</h2>
{error && <div className="appt-error">{error}</div>}
{success && <div className="appt-success">{success}</div>}
{error && <div className="appt-error">{error}</div>}
{success && <div className="appt-success">{success}</div>}
<label className="appt-label">
Store Location
<select
className="appt-select"
value={storeId}
onChange={(e) => setStoreId(e.target.value)}
required
<label className="appt-label">
Store Location
<select
className="appt-select"
value={storeId}
onChange={(e) => setStoreId(e.target.value)}
required
>
<option value="">Select a store...</option>
{stores.map((s) => (
<option key={s.id} value={s.id}>{s.label}</option>
))}
</select>
</label>
<label className="appt-label">
Service
<select
className="appt-select"
value={serviceId}
onChange={(e) => handleServiceChange(e.target.value)}
required
>
<option value="">Select a service...</option>
{services.map((s) => (
<option key={s.serviceId} value={s.serviceId}>
{s.serviceName} ${Number(s.servicePrice).toFixed(2)} ({s.serviceDuration} min)
</option>
))}
</select>
</label>
{selectedService && (
<div className="appt-service-info">
<p>{selectedService.serviceDesc}</p>
</div>
)}
<div className="appt-label">
Date
<DatePicker
value={appointmentDate}
minDate={getMinDate()}
onChange={setAppointmentDate}
/>
</div>
{storeId && serviceId && appointmentDate && (
<div className="appt-label">
<span>Available Time Slots</span>
{loadingSlots ? (
<p className="appt-slots-loading">Checking availability...</p>
) : availableSlots.length === 0 ? (
<p className="appt-no-slots">No available slots for this date. Please try another date.</p>
) : (
<div className="appt-slots-grid">
{availableSlots.map((slot) => (
<button
key={slot}
type="button"
className={`appt-slot-btn ${appointmentTime === slot ? "appt-slot-btn--selected" : ""}`}
onClick={() => setAppointmentTime(slot)}
>
{formatTime(slot)}
</button>
))}
</div>
)}
</div>
)}
{serviceId && (
<div className="appt-label">
<span>{petSectionLabel}</span>
{petsToShow.length === 0 ? (
<p className="appt-no-slots">{noPetsMessage}</p>
) : isAdoptionService ? (
<div className="appt-adopt-grid">
{petsToShow.map((p) => (
<label
key={p.petId}
className={`appt-adopt-card ${selectedPetIds.includes(p.petId) ? "appt-adopt-card--selected" : ""}`}
>
<input
type="radio"
name="adoptionPet"
value={p.petId}
checked={selectedPetIds.includes(p.petId)}
onChange={() => togglePet(p.petId)}
className="appt-adopt-radio"
/>
{p.imageUrl ? (
<img src={p.imageUrl} alt={p.petName} className="appt-adopt-img" />
) : (
<div className="appt-adopt-img-placeholder">🐾</div>
)}
<div className="appt-adopt-info">
<span className="appt-adopt-name">{p.petName}</span>
<span className="appt-adopt-detail">{p.petSpecies} · {p.petBreed}</span>
<span className="appt-adopt-detail">Age: {p.petAge}</span>
</div>
</label>
))}
</div>
) : (
<div className="appt-pets-grid">
{petsToShow.map((p) => (
<label
key={p.customerPetId}
className={`appt-pet-chip ${selectedPetIds.includes(p.customerPetId) ? "appt-pet-chip--selected" : ""}`}
>
<input
type="checkbox"
checked={selectedPetIds.includes(p.customerPetId)}
onChange={() => togglePet(p.customerPetId)}
className="appt-pet-checkbox"
/>
{p.petName}
<span className="appt-pet-chip-species">({p.species})</span>
</label>
))}
</div>
)}
</div>
)}
<button
type="submit"
className="appt-submit-btn"
disabled={!formValid || submitting}
>
<option value="">Select a store...</option>
{stores.map((s) => (
<option key={s.id} value={s.id}>{s.label}</option>
))}
</select>
</label>
<label className="appt-label">
Service
<select
className="appt-select"
value={serviceId}
onChange={(e) => handleServiceChange(e.target.value)}
required
>
<option value="">Select a service...</option>
{services.map((s) => (
<option key={s.serviceId} value={s.serviceId}>
{s.serviceName} ${Number(s.servicePrice).toFixed(2)} ({s.serviceDuration} min)
</option>
))}
</select>
</label>
{selectedService && (
{submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"}
</button>
</form>
) : (
<div className="appt-form">
<h2 className="appt-form-title">Appointment Booking</h2>
<div className="appt-service-info">
<p>{selectedService.serviceDesc}</p>
<p>Web appointment booking is currently available for customer accounts only.</p>
<p>Admin and staff accounts can still review appointment activity below.</p>
</div>
)}
<div className="appt-label">
Date
<DatePicker
value={appointmentDate}
minDate={getMinDate()}
onChange={setAppointmentDate}
/>
</div>
{storeId && serviceId && appointmentDate && (
<div className="appt-label">
<span>Available Time Slots</span>
{loadingSlots ? (
<p className="appt-slots-loading">Checking availability...</p>
) : availableSlots.length === 0 ? (
<p className="appt-no-slots">No available slots for this date. Please try another date.</p>
) : (
<div className="appt-slots-grid">
{availableSlots.map((slot) => (
<button
key={slot}
type="button"
className={`appt-slot-btn ${appointmentTime === slot ? "appt-slot-btn--selected" : ""}`}
onClick={() => setAppointmentTime(slot)}
>
{formatTime(slot)}
</button>
))}
</div>
)}
</div>
)}
{serviceId && (
<div className="appt-label">
<span>{petSectionLabel}</span>
{petsToShow.length === 0 ? (
<p className="appt-no-slots">{noPetsMessage}</p>
) : isAdoptionService ? (
<div className="appt-adopt-grid">
{petsToShow.map((p) => (
<label
key={p.petId}
className={`appt-adopt-card ${selectedPetIds.includes(p.petId) ? "appt-adopt-card--selected" : ""}`}
>
<input
type="radio"
name="adoptionPet"
value={p.petId}
checked={selectedPetIds.includes(p.petId)}
onChange={() => togglePet(p.petId)}
className="appt-adopt-radio"
/>
{p.imageUrl ? (
<img src={p.imageUrl} alt={p.petName} className="appt-adopt-img" />
) : (
<div className="appt-adopt-img-placeholder">🐾</div>
)}
<div className="appt-adopt-info">
<span className="appt-adopt-name">{p.petName}</span>
<span className="appt-adopt-detail">{p.petSpecies} · {p.petBreed}</span>
<span className="appt-adopt-detail">Age: {p.petAge}</span>
</div>
</label>
))}
</div>
) : (
<div className="appt-pets-grid">
{petsToShow.map((p) => (
<label
key={p.customerPetId}
className={`appt-pet-chip ${selectedPetIds.includes(p.customerPetId) ? "appt-pet-chip--selected" : ""}`}
>
<input
type="checkbox"
checked={selectedPetIds.includes(p.customerPetId)}
onChange={() => togglePet(p.customerPetId)}
className="appt-pet-checkbox"
/>
{p.petName}
<span className="appt-pet-chip-species">({p.species})</span>
</label>
))}
</div>
)}
</div>
)}
<button
type="submit"
className="appt-submit-btn"
disabled={!formValid || submitting}
>
{submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"}
</button>
</form>
)}
<div className="appt-history">
<h2 className="appt-form-title">Your Appointments</h2>
<h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
{loadingAppointments ? (
<p className="appt-loading">Loading appointments...</p>
) : appointments.length === 0 ? (
@@ -637,4 +662,8 @@ export default function AppointmentsPage() {
</section>
</main>
);
}
}
export default dynamic(() => Promise.resolve(AppointmentsPage), {
ssr: false,
});