diff --git a/backend/src/main/java/com/petshop/backend/controller/ContactController.java b/backend/src/main/java/com/petshop/backend/controller/ContactController.java new file mode 100644 index 00000000..c6b4cf1d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ContactController.java @@ -0,0 +1,40 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.EmailService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/contact") +public class ContactController { + + private final EmailService emailService; + private final UserRepository userRepository; + + public ContactController(EmailService emailService, UserRepository userRepository) { + this.emailService = emailService; + this.userRepository = userRepository; + } + + public record ContactRequest( + @NotBlank @Size(max = 150) String subject, + @NotBlank @Size(max = 2000) String body + ) {} + + @PostMapping + public ResponseEntity sendContactEmail(@Valid @RequestBody ContactRequest req) { + Long userId = AuthenticationHelper.getAuthenticatedUserId(); + User user = userRepository.findById(userId).orElseThrow(); + emailService.sendContactMessage(user, req.subject(), req.body()); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/EmailService.java b/backend/src/main/java/com/petshop/backend/service/EmailService.java index 667aad08..b944c053 100644 --- a/backend/src/main/java/com/petshop/backend/service/EmailService.java +++ b/backend/src/main/java/com/petshop/backend/service/EmailService.java @@ -129,6 +129,19 @@ public class EmailService { } } + public void sendContactMessage(User user, String subject, String body) { + if (user.getEmail() == null || user.getEmail().isBlank()) return; + String html = """ +
+

Contact form message

+

From: %s (%s)

+

Subject: %s

+
+

%s

+
""".formatted(esc(firstName(user)), esc(user.getEmail()), esc(subject), esc(body)); + send(user.getId(), user.getEmail(), "Contact: " + subject, html); + } + public void sendChatTranscript(Conversation conversation, List messages, User customer) { if (customer == null || customer.getEmail() == null || customer.getEmail().isBlank()) return; String subject = "Your PetShop support transcript"; diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 67170c59..1733fb45 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -2,6 +2,7 @@ import dynamic from "next/dynamic"; import { useState, useEffect, useCallback, useRef } from "react"; +import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -339,6 +340,7 @@ function AppointmentsPage() { const adoptionStoreName = searchParams.get("storeName") || ""; const didPreselectRef = useRef(false); + const errorRef = useRef(null); // Adoption-mode URL verification const [adoptionVerified, setAdoptionVerified] = useState(!adoptionMode); @@ -364,6 +366,12 @@ function AppointmentsPage() { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + useEffect(() => { + if (error && errorRef.current) { + errorRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, [error]); + const [appointments, setAppointments] = useState([]); const [loadingAppointments, setLoadingAppointments] = useState(false); @@ -425,12 +433,12 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; }) .then((r) => r.json()) .then(setStores) - .catch(() => {}); + .catch(() => setError("Failed to load stores.")); fetch(`${API_BASE}/api/v1/services?size=100`) .then((r) => r.json()) .then((data) => setServices(data.content ?? [])) - .catch(() => {}); + .catch(() => setError("Failed to load services.")); fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc&status=Available`) .then((r) => r.json()) @@ -779,7 +787,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";

{adoptionMode ? "New Adoption" : "New Appointment"}

- {error &&
{error}
} + {error &&
{error}
} {adoptionMode && adoptionVerifyLoading && (

Verifying pet details…

@@ -805,7 +813,10 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
Select Your Pet {eligiblePets.length === 0 ? ( -

You have no adopted pets available for appointments.

+

+ You have no adopted pets available.{" "} + Add a pet on your profile page. +

) : (
{eligiblePets.map((p) => ( diff --git a/web/app/cart/page.js b/web/app/cart/page.js index e555a5b2..344e50e7 100644 --- a/web/app/cart/page.js +++ b/web/app/cart/page.js @@ -43,6 +43,9 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) { try { await apiCompleteCheckout(token, paymentIntentId); } catch { + setPayError("Order confirmation failed. Please contact support."); + setPaying(false); + return; } onSuccess(); } diff --git a/web/app/chat/page.js b/web/app/chat/page.js index e54ea966..f49be420 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -57,7 +57,7 @@ function ChatPage() { } catch { - //Silent fail + setError("Failed to load messages."); } }, [token]); @@ -139,7 +139,7 @@ function ChatPage() { if (open) convId = open.id; } } catch { - // + setError("Failed to load conversations."); } } @@ -265,6 +265,7 @@ function ChatPage() { const isHuman = conversation?.mode === "HUMAN"; const hasStaff = !!conversation?.staffId; const isClosed = conversation?.status === "CLOSED"; + const hasStaffMessage = messages.some((m) => m.senderId !== user?.id); const staffStatusLabel = isClosed ? "Conversation closed" @@ -322,7 +323,7 @@ function ChatPage() {
- {!hasStaff && !isClosed && ( + {!hasStaff && !hasStaffMessage && !isClosed && (
A support agent will be with you shortly. You can send messages while you wait. diff --git a/web/app/contact/page.js b/web/app/contact/page.js index 19295ebd..79f9b555 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -1,12 +1,29 @@ "use client"; import { useState, useEffect } from "react"; +import { useAuth } from "@/context/AuthContext"; + +function getStoreImage(store) { + if (store.imageUrl) return store.imageUrl; + const name = store.storeName?.toLowerCase() ?? ""; + if (name.includes("downtown")) return "/stores/downtown.webp"; + if (name.includes("north")) return "/stores/north.webp"; + if (name.includes("west")) return "/stores/west.webp"; + return "/images/pet-placeholder.png"; +} export default function ContactPage() { + const { token } = useAuth(); const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [subject, setSubject] = useState(""); + const [body, setBody] = useState(""); + const [sending, setSending] = useState(false); + const [sendError, setSendError] = useState(null); + const [sendSuccess, setSendSuccess] = useState(false); + useEffect(() => { const params = new URLSearchParams({ page: "0", size: "100", sort: "storeName,asc" }); fetch(`/api/v1/stores?${params}`) @@ -19,6 +36,27 @@ export default function ContactPage() { .finally(() => setLoading(false)); }, []); + async function handleSend(e) { + e.preventDefault(); + setSending(true); + setSendError(null); + try { + const res = await fetch("/api/v1/contact", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ subject, body }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setSendSuccess(true); + setSubject(""); + setBody(""); + } catch (err) { + setSendError("Failed to send message. Please try again."); + } finally { + setSending(false); + } + } + return (
@@ -30,11 +68,49 @@ export default function ContactPage() {

General Contact

-

Email: support@petshop.com

-

Phone: (000) 000-0000

+

Email: hello@leonspetstore.com.au

+

Phone: (03) 9000 0000

Hours: Mon–Sat, 9:00 AM – 6:00 PM

+ {token && ( +
+

Send Us a Message

+ {sendSuccess ? ( +

Your message has been sent. We'll be in touch soon.

+ ) : ( + + +