merge main into branch

This commit is contained in:
2026-04-16 08:12:46 -06:00
49 changed files with 2187 additions and 400 deletions

View File

@@ -20,20 +20,22 @@ const SPECIES_BREEDS = {
Other: ["Other"],
};
// Explicit allowlists for species with restricted service availability.
// Species not listed here may use all services.
const SPECIES_SERVICE_ALLOWLIST = {
const SPECIES_EXCLUSIVE_SERVICES = {
Bird: ["wing clipping", "beak and nail"],
Fish: ["aquarium health"],
};
function getAvailableServices(services, species) {
if (!species) return services;
const allowlist = SPECIES_SERVICE_ALLOWLIST[species];
if (!allowlist) return services;
return services.filter((s) =>
allowlist.some((kw) => s.serviceName.toLowerCase().includes(kw))
);
return services.filter((s) => {
const name = s.serviceName.toLowerCase();
for (const [exclusiveSpecies, keywords] of Object.entries(SPECIES_EXCLUSIVE_SERVICES)) {
if (exclusiveSpecies !== species && keywords.some((kw) => name.includes(kw))) {
return false;
}
}
return true;
});
}
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
@@ -381,6 +383,10 @@ function AppointmentsPage() {
const [showAddPetModal, setShowAddPetModal] = useState(false);
const [cancellingId, setCancellingId] = useState(null);
const [apptSearch, setApptSearch] = useState("");
const [adoptionSearch, setAdoptionSearch] = useState("");
const [showPastAppts, setShowPastAppts] = useState(false);
const [showPastAdoptions, setShowPastAdoptions] = useState(false);
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
@@ -964,79 +970,164 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
<h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
{loadingAppointments ? (
<p className="appt-loading">Loading appointments...</p>
) : appointments.length === 0 ? (
<p className="appt-empty">No appointments yet.</p>
) : (
<div className="appt-list">
{appointments.map((a) => (
<div key={a.appointmentId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.serviceName}</span>
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
{a.appointmentStatus}
</span>
) : (() => {
const activeAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() === "booked");
const pastAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() !== "booked");
const q = apptSearch.toLowerCase();
const filteredActive = activeAppts.filter((a) =>
!q || [a.serviceName, a.storeName, a.petName].some((v) => v?.toLowerCase().includes(q))
);
return (
<>
<input
className="appt-search"
type="text"
placeholder="Search appointments…"
value={apptSearch}
onChange={(e) => setApptSearch(e.target.value)}
/>
{filteredActive.length === 0 ? (
<p className="appt-empty">{activeAppts.length === 0 ? "No active appointments." : "No results."}</p>
) : (
<div className="appt-list">
{filteredActive.map((a) => (
<div key={a.appointmentId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.serviceName}</span>
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
{a.appointmentStatus}
</span>
</div>
<div className="appt-card-details">
<span>{a.storeName}</span>
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
</div>
{a.petName && (
<div className="appt-card-pets">Pet: {a.petName}</div>
)}
<div className="appt-card-actions">
<button
type="button"
className="appt-cancel-btn"
disabled={cancellingId === a.appointmentId}
onClick={() => handleCancelAppointment(a.appointmentId)}
>
{cancellingId === a.appointmentId ? "Cancelling..." : "Cancel"}
</button>
</div>
</div>
))}
</div>
<div className="appt-card-details">
<span>{a.storeName}</span>
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
)}
{pastAppts.length > 0 && (
<div className="appt-past-section">
<button className="appt-past-toggle" onClick={() => setShowPastAppts((v) => !v)}>
{showPastAppts ? "Hide" : "Show"} past appointments ({pastAppts.length})
</button>
{showPastAppts && (
<div className="appt-list appt-list--past">
{pastAppts.map((a) => (
<div key={a.appointmentId} className="appt-card appt-card--past">
<div className="appt-card-header">
<span className="appt-card-service">{a.serviceName}</span>
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
{a.appointmentStatus}
</span>
</div>
<div className="appt-card-details">
<span>{a.storeName}</span>
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
</div>
{a.petName && (
<div className="appt-card-pets">Pet: {a.petName}</div>
)}
</div>
))}
</div>
)}
</div>
{a.petName && (
<div className="appt-card-pets">
Pet: {a.petName}
</div>
)}
{a.appointmentStatus?.toLowerCase() === "booked" && (
<div className="appt-card-actions">
<button
type="button"
className="appt-cancel-btn"
disabled={cancellingId === a.appointmentId}
onClick={() => handleCancelAppointment(a.appointmentId)}
>
{cancellingId === a.appointmentId ? "Cancelling..." : "Cancel"}
</button>
</div>
)}
</div>
))}
</div>
)}
)}
</>
);
})()}
<h2 className="appt-form-title" style={{ marginTop: "2rem" }}>{canBookAppointments ? "Your Adoptions" : "Adoptions"}</h2>
{loadingAdoptions ? (
<p className="appt-loading">Loading adoptions...</p>
) : adoptions.length === 0 ? (
<p className="appt-empty">No adoption requests yet.</p>
) : (
<div className="appt-list">
{adoptions.map((a) => (
<div key={a.adoptionId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.petName}</span>
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
{a.adoptionStatus}
</span>
) : (() => {
const activeAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() === "pending");
const pastAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() !== "pending");
const q = adoptionSearch.toLowerCase();
const filteredActive = activeAdoptions.filter((a) =>
!q || [a.petName, a.sourceStoreName].some((v) => v?.toLowerCase().includes(q))
);
return (
<>
<input
className="appt-search"
type="text"
placeholder="Search adoptions…"
value={adoptionSearch}
onChange={(e) => setAdoptionSearch(e.target.value)}
/>
{filteredActive.length === 0 ? (
<p className="appt-empty">{activeAdoptions.length === 0 ? "No active adoption requests." : "No results."}</p>
) : (
<div className="appt-list">
{filteredActive.map((a) => (
<div key={a.adoptionId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.petName}</span>
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
{a.adoptionStatus}
</span>
</div>
<div className="appt-card-details">
<span>{a.sourceStoreName}</span>
<span>{a.adoptionDate}</span>
</div>
<div className="appt-card-actions">
<button
type="button"
className="appt-cancel-btn"
disabled={cancellingId === a.adoptionId}
onClick={() => handleCancelAdoption(a.adoptionId)}
>
{cancellingId === a.adoptionId ? "Cancelling..." : "Cancel"}
</button>
</div>
</div>
))}
</div>
<div className="appt-card-details">
<span>{a.sourceStoreName}</span>
<span>{a.adoptionDate}</span>
)}
{pastAdoptions.length > 0 && (
<div className="appt-past-section">
<button className="appt-past-toggle" onClick={() => setShowPastAdoptions((v) => !v)}>
{showPastAdoptions ? "Hide" : "Show"} past adoptions ({pastAdoptions.length})
</button>
{showPastAdoptions && (
<div className="appt-list appt-list--past">
{pastAdoptions.map((a) => (
<div key={a.adoptionId} className="appt-card appt-card--past">
<div className="appt-card-header">
<span className="appt-card-service">{a.petName}</span>
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
{a.adoptionStatus}
</span>
</div>
<div className="appt-card-details">
<span>{a.sourceStoreName}</span>
<span>{a.adoptionDate}</span>
</div>
</div>
))}
</div>
)}
</div>
{a.adoptionStatus?.toLowerCase() === "pending" && (
<div className="appt-card-actions">
<button
type="button"
className="appt-cancel-btn"
disabled={cancellingId === a.adoptionId}
onClick={() => handleCancelAdoption(a.adoptionId)}
>
{cancellingId === a.adoptionId ? "Cancelling..." : "Cancel"}
</button>
</div>
)}
</div>
))}
</div>
)}
)}
</>
);
})()}
</div>
</section>
</main>