From 1c0f55fbe5f12dd9831aa2286134f26381f83e37 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Wed, 25 Mar 2026 08:19:44 -0600 Subject: [PATCH 001/137] Adopt page, minor adjustment to backend --- .../backend/security/SecurityConfig.java | 32 +- web/app/adopt/[id]/page.js | 52 ++ web/app/adopt/page.js | 149 ++++++ web/app/globals.css | 487 +++++++++++++++++- web/app/page.js | 12 +- web/components/Navigation.js | 2 +- web/components/PetCard.js | 21 + web/components/PetProfile.js | 56 ++ web/components/petUtils.js | 54 ++ web/next.config.mjs | 9 +- 10 files changed, 858 insertions(+), 16 deletions(-) create mode 100644 web/app/adopt/[id]/page.js create mode 100644 web/app/adopt/page.js create mode 100644 web/components/PetCard.js create mode 100644 web/components/PetProfile.js create mode 100644 web/components/petUtils.js diff --git a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java index 00ce63f8..d26e3e84 100644 --- a/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java +++ b/backend/src/main/java/com/petshop/backend/security/SecurityConfig.java @@ -17,6 +17,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -28,12 +33,10 @@ public class SecurityConfig { private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; private final RestAccessDeniedHandler restAccessDeniedHandler; - public SecurityConfig( - JwtAuthenticationFilter jwtAuthFilter, + public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, UserDetailsService userDetailsService, RestAuthenticationEntryPoint restAuthenticationEntryPoint, - RestAccessDeniedHandler restAccessDeniedHandler - ) { + RestAccessDeniedHandler restAccessDeniedHandler) { this.jwtAuthFilter = jwtAuthFilter; this.userDetailsService = userDetailsService; this.restAuthenticationEntryPoint = restAuthenticationEntryPoint; @@ -42,7 +45,7 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http + http.cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll() @@ -56,8 +59,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll() .anyRequest().authenticated() ) - .exceptionHandling(ex -> ex - .authenticationEntryPoint(restAuthenticationEntryPoint) + .exceptionHandling(ex -> ex.authenticationEntryPoint(restAuthenticationEntryPoint) .accessDeniedHandler(restAccessDeniedHandler) ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) @@ -70,16 +72,32 @@ public class SecurityConfig { private DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return new ProviderManager(daoAuthenticationProvider()); } + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return source; + } + @Bean public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); } } diff --git a/web/app/adopt/[id]/page.js b/web/app/adopt/[id]/page.js new file mode 100644 index 00000000..21a24aa4 --- /dev/null +++ b/web/app/adopt/[id]/page.js @@ -0,0 +1,52 @@ +"use client"; + +import Link from "next/link"; +import { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; +import PetProfile from "@/components/PetProfile"; + +const API_BASE = ""; + +export default function PetDetailPage() { + const { id } = useParams(); + const [pet, setPet] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) return; + setLoading(true); + setError(null); + + fetch(`${API_BASE}/api/v1/pets/${id}`) + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status} – ${res.statusText}`); + return res.json(); + }) + .then((data) => setPet(data)) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, [id]); + + return ( +
+
+ ← Back to Pets + + {loading &&

Loading pet details...

} + {error &&

{error}

} + + {!loading && !error && pet && ( + + )} +
+
+ ); +} diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js new file mode 100644 index 00000000..9a30f6ea --- /dev/null +++ b/web/app/adopt/page.js @@ -0,0 +1,149 @@ +"use client"; + +import { useState, useEffect } from "react"; +import PetCard from "@/components/PetCard"; + +const API_BASE = ""; + +export default function AdoptPage() { + const [pets, setPets] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [health, setHealth] = useState(null); + const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); + const [page, setPage] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + const PAGE_SIZE = 12; + + useEffect(() => { + fetch(`${API_BASE}/api/v1/health`) + .then((res) => (res.ok ? setHealth("online") : setHealth("error"))) + .catch(() => setHealth("offline")); + }, []); + + useEffect(() => { + setLoading(true); + setError(null); + + const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "id,asc" }); + if (query) params.set("q", query); + + fetch(`${API_BASE}/api/v1/pets?${params}`) + .then((res) => { + if (!res.ok) throw new Error(`HTTP ${res.status} – ${res.statusText}`); + return res.json(); + }) + .then((data) => { + setPets(data.content ?? []); + setTotalPages(data.totalPages ?? 0); + }) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, [page, query]); + + function handleSearch(e) { + e.preventDefault(); + setPage(0); + setQuery(search.trim()); + } + + return ( +
+
+

Find Your Perfect Companion

+

Give a loving pet their forever home

+
+
+ +
+
+
+ setSearch(e.target.value)} + /> + + {query && ( + + )} +
+ +
+
+ +
+ {loading &&

Loading pets...

} + + {error && ( +
+

Failed to load pets

+ {error} +

+ {health === "offline" + ? "The Spring Boot backend is not reachable. Make sure it is running in IntelliJ on port 8080." + : health === "error" + ? "The backend responded with an error. Check the IntelliJ Run console for stack traces." + : "The backend is reachable but the /pets endpoint failed. Check the IntelliJ Run console."} +

+
+ )} + + {!loading && !error && pets.length === 0 && ( +

No pets found.

+ )} + + {!loading && !error && pets.length > 0 && ( +
+ {pets.map((pet) => ( + + ))} +
+ )} + + {!loading && totalPages > 1 && ( +
+ + Page {page + 1} of {totalPages} + +
+ )} +
+
+ ); +} diff --git a/web/app/globals.css b/web/app/globals.css index fd71a692..7e93dfb2 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -232,7 +232,424 @@ body { border-radius: 2px; } +/* ─── Adopt Page ─────────────────────────────────────────────── */ + +.adopt-page { + min-height: 100vh; +} + +.adopt-hero { + text-align: center; + padding: 4rem 2rem 3rem; + background: linear-gradient(to bottom, #f9f9f9, #ffffff); +} + +.adopt-hero-title { + font-size: 3rem; + color: #333; + margin-bottom: 1rem; + font-weight: 700; + letter-spacing: -0.5px; +} + +.adopt-hero-subtitle { + font-size: 1.5rem; + color: #666; + margin-bottom: 2rem; + font-weight: 300; +} + +.adopt-controls { + max-width: 1200px; + margin: 0 auto 1.5rem; + padding: 0 2rem; +} + +.adopt-search-form { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.adopt-search-input { + flex: 1; + max-width: 400px; + padding: 0.6rem 1rem; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 1rem; + font-family: Arial, sans-serif; + transition: border-color 0.2s ease; + outline: none; +} + +.adopt-search-input:focus { + border-color: orange; +} + +.adopt-search-btn { + padding: 0.6rem 1.4rem; + background: orange; + color: white; + border: none; + border-radius: 6px; + font-size: 1rem; + font-family: Arial, sans-serif; + cursor: pointer; + transition: background 0.2s ease; +} + +.adopt-search-btn:hover { + background: #e69500; +} + +.adopt-clear-btn { + padding: 0.6rem 1rem; + background: transparent; + color: #666; + border: 2px solid #ddd; + border-radius: 6px; + font-size: 1rem; + font-family: Arial, sans-serif; + cursor: pointer; + transition: all 0.2s ease; +} + +.adopt-clear-btn:hover { + border-color: #aaa; + color: #333; +} + +.adopt-grid-section { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem 4rem; +} + +.adopt-status-msg { + text-align: center; + color: #666; + font-size: 1.1rem; + padding: 3rem 0; +} + +.adopt-error { + color: #c0392b; +} + +.adopt-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.75rem; +} + +.pet-card { + text-decoration: none; + color: inherit; + display: flex; + flex-direction: column; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transition: transform 0.3s ease, box-shadow 0.3s ease; + background: #fff; +} + +.pet-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13); +} + +.pet-card-image-wrapper { + background: #fff8ee; + display: flex; + align-items: center; + justify-content: center; + height: 160px; +} + +.pet-card-emoji { + font-size: 5rem; + line-height: 1; +} + +.pet-card-body { + padding: 1rem 1.25rem 1.25rem; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.pet-card-name { + font-size: 1.2rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.pet-card-species { + font-size: 0.95rem; + color: #666; + margin: 0; +} + +.pet-card-status { + display: inline-block; + margin-top: 0.4rem; + padding: 0.2rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; + text-transform: capitalize; + width: fit-content; +} + +.status-available { + background: #e6f9ee; + color: #1a7a3c; +} + +.status-adopted { + background: #ffe8e8; + color: #c0392b; +} + +.status-pending { + background: #fff4e0; + color: #b36b00; +} + +.status-other { + background: #f0f0f0; + color: #555; +} + +.adopt-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 1.5rem; + margin-top: 3rem; +} + +.pagination-btn { + padding: 0.5rem 1.2rem; + background: orange; + color: white; + border: none; + border-radius: 6px; + font-size: 0.95rem; + font-family: Arial, sans-serif; + cursor: pointer; + transition: background 0.2s ease; +} + +.pagination-btn:hover:not(:disabled) { + background: #e69500; +} + +.pagination-btn:disabled { + background: #ddd; + color: #aaa; + cursor: default; +} + +.pagination-info { + font-size: 0.95rem; + color: #555; +} + +/* ─── Pet Detail Page ─────────────────────────────────────────── */ + +.pet-detail-page { + min-height: 100vh; + padding: 3rem 2rem 5rem; +} + +.pet-detail-container { + max-width: 860px; + margin: 0 auto; +} + +.pet-detail-back { + display: inline-block; + margin-bottom: 2rem; + color: orange; + text-decoration: none; + font-size: 1rem; + font-weight: 600; + transition: color 0.2s ease; +} + +.pet-detail-back:hover { + color: #e69500; +} + +.pet-detail-card { + display: flex; + gap: 3rem; + background: #fff; + border-radius: 20px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.pet-detail-image-wrapper { + flex-shrink: 0; + width: 280px; + background: #fff8ee; + display: flex; + align-items: center; + justify-content: center; +} + +.pet-detail-emoji { + font-size: 8rem; + line-height: 1; +} + +.pet-detail-info { + flex: 1; + padding: 2.5rem 2.5rem 2.5rem 0; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.pet-detail-header { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.pet-detail-name { + font-size: 2.2rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.pet-detail-fields { + display: flex; + flex-direction: column; + gap: 0; + border: 1px solid #eee; + border-radius: 10px; + overflow: hidden; +} + +.pet-detail-row { + display: flex; + align-items: center; + padding: 0.85rem 1.25rem; + border-bottom: 1px solid #eee; +} + +.pet-detail-row:last-child { + border-bottom: none; +} + +.pet-detail-label { + width: 140px; + font-size: 0.9rem; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +.pet-detail-value { + font-size: 1rem; + color: #333; +} + +.pet-detail-price { + font-weight: 700; + color: #1a7a3c; + font-size: 1.1rem; +} + +.pet-detail-cta { + background: #fff8ee; + border-radius: 12px; + padding: 1.25rem 1.5rem; +} + +.pet-detail-cta-text { + font-size: 0.95rem; + color: #555; + margin: 0 0 1rem; +} + +.pet-detail-cta-btn { + display: inline-block; + padding: 0.65rem 1.5rem; + background: orange; + color: white; + text-decoration: none; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + transition: background 0.2s ease; +} + +.pet-detail-cta-btn:hover { + background: #e69500; +} + +/* ─── Responsive Design ──────────────────────────────────────── */ /* Responsive Design */ +@media (max-width: 1024px) { + .adopt-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .adopt-hero-title { + font-size: 2rem; + } + + .adopt-hero-subtitle { + font-size: 1.2rem; + } + + .adopt-grid { + grid-template-columns: repeat(2, 1fr); + gap: 1.25rem; + } + + .pet-detail-card { + flex-direction: column; + gap: 0; + } + + .pet-detail-image-wrapper { + width: 100%; + height: 200px; + } + + .pet-detail-info { + padding: 1.75rem; + } +} + +@media (max-width: 480px) { + .adopt-grid { + grid-template-columns: 1fr; + } + + .adopt-hero-title { + font-size: 1.6rem; + } + + .pet-detail-name { + font-size: 1.7rem; + } +} + @media (max-width: 1024px) { .slideshow-container { height: 400px; @@ -287,4 +704,72 @@ body { .centered-title-section { padding: 2rem 1rem; } -} \ No newline at end of file +} +/* ─── Adopt diagnostic additions ────────────────────────────── */ + +.adopt-controls-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; +} + +.backend-status { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.backend-status--online { + background: #1a7a3c; +} + +.backend-status--offline { + background: #c0392b; +} + +.backend-status--error { + background: #e67e00; +} + +.backend-status--checking { + background: #bbb; +} + +.adopt-error-box { + max-width: 640px; + margin: 3rem auto; + padding: 1.5rem 2rem; + background: #fff8f8; + border: 2px solid #f5c6c6; + border-radius: 12px; +} + +.adopt-error-title { + font-size: 1.1rem; + font-weight: 700; + color: #c0392b; + margin: 0 0 0.6rem; +} + +.adopt-error-detail { + display: block; + font-family: monospace; + font-size: 0.9rem; + background: #fff0f0; + border: 1px solid #f5c6c6; + border-radius: 6px; + padding: 0.5rem 0.75rem; + margin-bottom: 0.9rem; + word-break: break-all; +} + +.adopt-error-hint { + font-size: 0.9rem; + color: #555; + margin: 0; + line-height: 1.5; +} diff --git a/web/app/page.js b/web/app/page.js index bf0875e6..6aea5518 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -5,7 +5,7 @@ import Link from "next/link"; import { useState, useEffect } from "react"; export default function Home() { - // Slideshow images array + //Slideshow images array const slideshowImages = [ { src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets" }, { src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies" }, @@ -15,7 +15,7 @@ export default function Home() { const [currentSlide, setCurrentSlide] = useState(0); - // Auto-advance slideshow + //Auto-advance slideshow useEffect(() => { //Change slide every 7.5 seconds const timer = setInterval(() => {setCurrentSlide((prev) => (prev + 1) % slideshowImages.length);}, 7500); @@ -23,7 +23,7 @@ export default function Home() { return () => clearInterval(timer); }, [slideshowImages.length]); - // Four images that link to other pages + //Hyperlinks to other pages const navImages = [ { src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet" }, { src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store" }, @@ -33,7 +33,7 @@ export default function Home() { return (
- {/* Slideshow Section */} + {/* Slideshow */}
{slideshowImages.map((image, index) => (
- {/* Centered Title Section */} + {/* Title Section */}

Welcome to Leon's Pet Store

Your One-Stop Shop for All Things Pets

- {/* Four Image Links Section */} + {/* Image Hyperlinks */}
{navImages.map((item, index) => ( diff --git a/web/components/Navigation.js b/web/components/Navigation.js index 4780df6c..f7de55e8 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -15,7 +15,7 @@ export default function DisplayNav() {
Home - Adopt a Pet + Adopt a Pet Online Store Schedule an Appointment Contact Us diff --git a/web/components/PetCard.js b/web/components/PetCard.js new file mode 100644 index 00000000..5ccb0219 --- /dev/null +++ b/web/components/PetCard.js @@ -0,0 +1,21 @@ +//Pet cards (on adopt page) + +import Link from "next/link"; +import { getSpeciesEmoji, getStatusClass } from "@/components/petUtils"; + +export default function PetCard({petId, petName, petSpecies, petStatus}) { + return ( + +
+ {getSpeciesEmoji(petSpecies)} +
+
+

{petName}

+

{petSpecies}

+ + {petStatus} + +
+ + ); +} diff --git a/web/components/PetProfile.js b/web/components/PetProfile.js new file mode 100644 index 00000000..87d4b059 --- /dev/null +++ b/web/components/PetProfile.js @@ -0,0 +1,56 @@ +import Link from "next/link"; +import { getSpeciesEmoji, getStatusClass } from "@/components/petUtils"; + +export default function PetProfile({ petName, petSpecies, petBreed, petAge, petStatus, petPrice }) { + return ( +
+
+ {getSpeciesEmoji(petSpecies)} +
+ +
+
+

{petName}

+ + {petStatus} + +
+ +
+
+ Species + {petSpecies ?? "—"} +
+
+ Breed + {petBreed ?? "—"} +
+
+ Age + + {petAge != null ? `${petAge} ${petAge === 1 ? "year" : "years"}` : "—"} + +
+
+ Adoption Fee + + {petPrice != null ? `$${parseFloat(petPrice).toFixed(2)}` : "—"} + +
+
+ + {/* Status */} + {petStatus?.toLowerCase() === "available" && ( +
+

+ Interested in adopting {petName}? Visit us in store or schedule an appointment. +

+ + Schedule an Appointment + +
+ )} +
+
+ ); +} diff --git a/web/components/petUtils.js b/web/components/petUtils.js new file mode 100644 index 00000000..4a07b30e --- /dev/null +++ b/web/components/petUtils.js @@ -0,0 +1,54 @@ + +//Temporary, until image support is added +export const SPECIES_EMOJI = { + dog: "🐶", + cat: "🐱", + bird: "🐦", + rabbit: "🐰", + hamster: "🐹", + fish: "🐟", + turtle: "🐢", + snake: "🐍", + lizard: "🦎", + guinea: "🐹", +}; + +export function getSpeciesEmoji(species) { + if (!species) { + + return "🐾"; + } + + const key = species.toLowerCase(); + + for (const [k, v] of Object.entries(SPECIES_EMOJI)) { + if (key.includes(k)) { + + return v; + } + } + + return "🐾"; +} + +export function getStatusClass(status) { + if (!status) { + return ""; + } + + const s = status.toLowerCase(); + + if (s === "available") { + return "status-available"; + } + + if (s === "adopted") { + return "status-adopted"; + } + + if (s === "pending") { + return "status-pending"; + } + + return "status-other"; +} diff --git a/web/next.config.mjs b/web/next.config.mjs index 690d2d0d..b725bf69 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -1,7 +1,14 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - /* config options here */ reactCompiler: true, + async rewrites() { + return [ + { + source: "/api/:path*", + destination: "http://localhost:8080/api/:path*", + }, + ]; + }, }; export default nextConfig; From c048c4bdc3ae6d094c24cab9bacdb5f4522aac6b Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Sun, 29 Mar 2026 17:43:31 -0600 Subject: [PATCH 002/137] Account login --- web/app/globals.css | 247 ++++++++++++++++++++++++++++++ web/app/layout.js | 9 +- web/app/login/page.js | 75 +++++++++ web/app/page.js | 16 +- web/app/profile/page.js | 61 ++++++++ web/app/register/page.js | 151 ++++++++++++++++++ web/components/ClientProviders.js | 7 + web/components/Navigation.js | 65 +++++--- web/context/AuthContext.js | 107 +++++++++++++ 9 files changed, 708 insertions(+), 30 deletions(-) create mode 100644 web/app/login/page.js create mode 100644 web/app/profile/page.js create mode 100644 web/app/register/page.js create mode 100644 web/components/ClientProviders.js create mode 100644 web/context/AuthContext.js diff --git a/web/app/globals.css b/web/app/globals.css index 7e93dfb2..465c429d 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -773,3 +773,250 @@ body { margin: 0; line-height: 1.5; } + +/* Auth/nav */ + +.navbar { + justify-content: space-between; +} + +.nav-auth { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + padding-left: 1.5rem; + flex-shrink: 0; +} + +.nav-greeting { + font-weight: 600; + white-space: nowrap; +} + +.nav-register-btn { + background: white; + color: orange !important; + font-weight: 600; + border-radius: 20px; + padding: 0.4rem 1rem !important; +} + +.nav-register-btn:hover { + background: #fff3e0 !important; +} + +.nav-logout-btn { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: 20px; + padding: 0.35rem 1rem; + font-size: 0.95rem; + cursor: pointer; + transition: background 0.2s ease; + white-space: nowrap; +} + +.nav-logout-btn:hover { + background: rgba(255, 255, 255, 0.35); +} + +/* Login/Register */ + +.auth-page { + min-height: calc(100vh - 70px); + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + background: #fafafa; +} + +.auth-card { + background: white; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); + padding: 2.5rem; + width: 100%; + max-width: 440px; +} + +.auth-title { + font-size: 1.75rem; + font-weight: 700; + color: #222; + margin: 0 0 1.5rem; + text-align: center; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.auth-label { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.9rem; + font-weight: 600; + color: #444; +} + +.auth-input { + padding: 0.6rem 0.85rem; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; +} + +.auth-input:focus { + border-color: orange; + box-shadow: 0 0 0 3px rgba(255, 165, 0, 0.2); +} + +.auth-submit-btn { + margin-top: 0.5rem; + padding: 0.75rem; + background: orange; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; +} + +.auth-submit-btn:hover:not(:disabled) { + background: #e69500; +} + +.auth-submit-btn:active:not(:disabled) { + transform: scale(0.98); +} + +.auth-submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.auth-error { + background: #fff0f0; + border: 1px solid #f5c6c6; + color: #c0392b; + border-radius: 8px; + padding: 0.65rem 1rem; + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.auth-switch { + text-align: center; + font-size: 0.9rem; + color: #666; + margin-top: 1.25rem; +} + +.auth-switch-link { + color: orange; + font-weight: 600; + text-decoration: none; +} + +.auth-switch-link:hover { + text-decoration: underline; +} + +/* User Profile Page */ + +.profile-card { + background: white; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); + padding: 2.5rem; + width: 100%; + max-width: 480px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +.profile-avatar-circle { + width: 80px; + height: 80px; + border-radius: 50%; + background: orange; + color: white; + font-size: 2rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.25rem; +} + +.profile-name { + font-size: 1.5rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.profile-role-badge { + display: inline-block; + background: #fff3e0; + color: #e67e00; + border: 1px solid #ffd180; + border-radius: 20px; + padding: 0.2rem 0.85rem; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.profile-fields { + width: 100%; + margin: 0.75rem 0 0; + border-top: 1px solid #eee; + padding-top: 1rem; +} + +.profile-field-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 0.55rem 0; + border-bottom: 1px solid #f0f0f0; + gap: 1rem; +} + +.profile-field-label { + font-size: 0.85rem; + font-weight: 600; + color: #888; + flex-shrink: 0; +} + +.profile-field-value { + font-size: 0.95rem; + color: #222; + text-align: right; + word-break: break-word; +} + +.profile-logout-btn { + width: 100%; + margin-top: 1rem; +} + +.profile-loading { + color: #888; + font-size: 1rem; +} diff --git a/web/app/layout.js b/web/app/layout.js index 86efbb81..0a50c8db 100644 --- a/web/app/layout.js +++ b/web/app/layout.js @@ -1,18 +1,21 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import DisplayNav from "@/components/Navigation"; +import ClientProviders from "@/components/ClientProviders"; export const metadata = { title: "Leon's Pet Store", description: "Generated by create next app", }; -export default function RootLayout({ children }) { +export default function RootLayout({children}) { return ( - - {children} + + + {children} + ); diff --git a/web/app/login/page.js b/web/app/login/page.js new file mode 100644 index 00000000..32c05c52 --- /dev/null +++ b/web/app/login/page.js @@ -0,0 +1,75 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/context/AuthContext"; + +export default function LoginPage() { + const {login} = useAuth(); + const router = useRouter(); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + await login(username, password); + router.push("/"); + } + + catch (err) { + setError(err.message); + } + + finally { + setLoading(false); + } + } + + return ( +
+
+

Log In

+ + {error &&

{error}

} + +
+ + + + + +
+ +

+ Don't have an account?{" "} + Register here +

+
+
+ ); +} diff --git a/web/app/page.js b/web/app/page.js index 6aea5518..4819bd7d 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -7,10 +7,10 @@ import { useState, useEffect } from "react"; export default function Home() { //Slideshow images array const slideshowImages = [ - { src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets" }, - { src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies" }, - { src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming" }, - { src: "/images/home/slideshow/pet4.jpg", alt: "Pet food" }, + {src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets"}, + {src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies"}, + {src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming"}, + {src: "/images/home/slideshow/pet4.jpg", alt: "Pet food"}, ]; const [currentSlide, setCurrentSlide] = useState(0); @@ -25,10 +25,10 @@ export default function Home() { //Hyperlinks to other pages const navImages = [ - { src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet" }, - { src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store" }, - { src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments" }, - { src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us" }, + {src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet"}, + {src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store"}, + {src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments"}, + {src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us"}, ]; return ( diff --git a/web/app/profile/page.js b/web/app/profile/page.js new file mode 100644 index 00000000..fcc903cc --- /dev/null +++ b/web/app/profile/page.js @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/context/AuthContext"; + +export default function ProfilePage() { + const { user, loading, logout } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!loading && !user) { + router.replace("/login"); + } + }, [user, loading, router]); + + function handleLogout() { + logout(); + + router.push("/"); + } + + if (loading || !user) { + return

Loading…

; + } + + const fields = [ + {label: "Full Name", value: user.fullName}, + {label: "Username", value: user.username}, + {label: "Email", value: user.email}, + {label: "Phone", value: user.phone || "—"}, + {label: "Role", value: user.role}, + ...(user.storeName ? [{label: "Store", value: user.storeName}] : []), + ]; + + return ( +
+
+
+ {(user.fullName || user.username).charAt(0).toUpperCase()} +
+ +

{user.fullName || user.username}

+ {user.role} + +
+ {fields.map(({ label, value }) => ( +
+
{label}
+
{value}
+
+ ))} +
+ + +
+
+ ); +} diff --git a/web/app/register/page.js b/web/app/register/page.js new file mode 100644 index 00000000..20dcd2c1 --- /dev/null +++ b/web/app/register/page.js @@ -0,0 +1,151 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/context/AuthContext"; + +export default function RegisterPage() { + const {register} = useAuth(); + const router = useRouter(); + + const [form, setForm] = useState({ + fullName: "", + username: "", + email: "", + phone: "", + password: "", + confirmPassword: "",}); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + function handleChange(e) { + setForm((prev) => ({ ...prev, [e.target.name]: e.target.value })); + } + + async function handleSubmit(e) { + e.preventDefault(); + setError(""); + + if (form.password !== form.confirmPassword) { + setError("Passwords do not match."); + return; + } + + setLoading(true); + try { + await register({fullName: form.fullName, + username: form.username, + email: form.email, + phone: form.phone, + password: form.password, + }); + router.push("/"); + } + + catch (err) { + setError(err.message); + } + + finally { + setLoading(false); + } + } + + return ( +
+
+

Create Account

+ + {error &&

{error}

} + +
+ + + + + + + + + + + + + +
+ +

+ Already have an account?{" "} + Log in here +

+
+
+ ); +} diff --git a/web/components/ClientProviders.js b/web/components/ClientProviders.js new file mode 100644 index 00000000..64e8157a --- /dev/null +++ b/web/components/ClientProviders.js @@ -0,0 +1,7 @@ +"use client"; + +import { AuthProvider } from "@/context/AuthContext"; + +export default function ClientProviders({children}) { + return {children}; +} diff --git a/web/components/Navigation.js b/web/components/Navigation.js index f7de55e8..2e7f7cac 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -1,26 +1,53 @@ -import Link from "next/link"; +"use client"; + import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/context/AuthContext"; export default function DisplayNav() { + const {user, logout, loading} = useAuth(); + const router = useRouter(); + + function handleLogout() { + logout(); + router.push("/"); + } + return ( + + + + +
+ {loading ? null : user ? ( + <> + + Hello, {user.fullName || user.username} + + + + ) : ( + <> + Log In + Register + + )} +
+ ); } \ No newline at end of file diff --git a/web/context/AuthContext.js b/web/context/AuthContext.js new file mode 100644 index 00000000..5a9b0d08 --- /dev/null +++ b/web/context/AuthContext.js @@ -0,0 +1,107 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, useCallback } from "react"; + +const AuthContext = createContext(null); + +const TOKEN_KEY = "auth_token"; + +async function fetchCurrentUser(token) { + const res = await fetch("/api/v1/auth/me", { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return null; + return res.json(); +} + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const stored = localStorage.getItem(TOKEN_KEY); + if (!stored) { + setLoading(false); + + return; + } + fetchCurrentUser(stored) + .then((data) => { + if (data) { + setToken(stored); + setUser(data); + } + + else { + localStorage.removeItem(TOKEN_KEY); + } + }).catch(() => localStorage.removeItem(TOKEN_KEY)).finally(() => setLoading(false));}, []); + + const login = useCallback(async (username, password) => { + const res = await fetch("/api/v1/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message || "Login failed"); + } + + const jwt = data.token; + localStorage.setItem(TOKEN_KEY, jwt); + setToken(jwt); + + const userInfo = await fetchCurrentUser(jwt); + + setUser(userInfo); + + return userInfo; + }, []); + + const register = useCallback(async ({ username, password, email, fullName, phone }) => { + const res = await fetch("/api/v1/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password, email, fullName, phone }), + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.message || "Registration failed"); + } + + const jwt = data.token; + + localStorage.setItem(TOKEN_KEY, jwt); + setToken(jwt); + + const userInfo = await fetchCurrentUser(jwt); + setUser(userInfo); + + return userInfo; + }, []); + + const logout = useCallback(() => { + localStorage.removeItem(TOKEN_KEY); + setToken(null); + setUser(null);}, []); + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error("useAuth must be used within an AuthProvider"); + } + + return ctx; +} From ab97a86977ead74ce10b43ee3e435ca793a225b2 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 19:02:12 -0600 Subject: [PATCH 003/137] Add chat close endpoint --- .../backend/controller/ChatController.java | 9 ++ .../petshop/backend/service/ChatService.java | 30 +++++ .../backend/service/ChatServiceTest.java | 126 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index 8cfb5df3..59b4f74f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -96,4 +96,13 @@ public class ChatController { chatRealtimeService.publishConversationUpdate(id); return ResponseEntity.ok(conversation); } + + @PostMapping("/conversations/{id}/close") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity closeConversation(@PathVariable Long id) { + User user = getCurrentUser(); + ConversationResponse conversation = chatService.closeConversation(id, user.getId(), user.getRole()); + chatRealtimeService.publishConversationUpdate(id); + return ResponseEntity.ok(conversation); + } } diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index f66cbdb5..08c94478 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -116,6 +116,10 @@ public class ChatService { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) { + throw new AccessDeniedException("Conversation is closed"); + } + if (!hasConversationAccess(conversation, userId, role)) { if (role == User.Role.CUSTOMER) { throw new AccessDeniedException("You can only send messages to your own conversations"); @@ -149,6 +153,10 @@ public class ChatService { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + if (conversation.getStatus() == Conversation.ConversationStatus.CLOSED) { + throw new AccessDeniedException("Conversation is closed"); + } + if (role != User.Role.CUSTOMER || !hasConversationAccess(conversation, userId, role)) { throw new AccessDeniedException("You can only request human takeover for your own conversations"); } @@ -163,6 +171,28 @@ public class ChatService { return ConversationResponse.fromEntity(conversation, lastMessage); } + @Transactional + public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) { + Conversation conversation = conversationRepository.findById(conversationId) + .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); + + if (!hasConversationAccess(conversation, userId, role)) { + if (role == User.Role.CUSTOMER) { + throw new AccessDeniedException("You can only close your own conversations"); + } + if (role == User.Role.STAFF) { + throw new AccessDeniedException("You can only close conversations assigned to you or unassigned conversations"); + } + } + + conversation.setStatus(Conversation.ConversationStatus.CLOSED); + conversation = conversationRepository.save(conversation); + + List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); + String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + return ConversationResponse.fromEntity(conversation, lastMessage); + } + public List getMessages(Long conversationId, Long userId, User.Role role) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); diff --git a/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java b/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java new file mode 100644 index 00000000..323d4c04 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java @@ -0,0 +1,126 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.chat.ConversationRequest; +import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.entity.Conversation; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Message; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.ConversationRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.MessageRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.access.AccessDeniedException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ChatServiceTest { + + @Mock + private ConversationRepository conversationRepository; + + @Mock + private MessageRepository messageRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private CustomerRepository customerRepository; + + @InjectMocks + private ChatService chatService; + + private Customer customer; + + @BeforeEach + void setUp() { + customer = new Customer(); + customer.setCustomerId(1L); + customer.setUserId(10L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + customer.setEmail("pat@example.com"); + } + + @Test + void closeConversationMarksConversationClosed() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)) + .thenReturn(List.of(message("hello"))); + + var response = chatService.closeConversation(99L, 10L, User.Role.CUSTOMER); + + assertEquals("CLOSED", response.getStatus()); + assertEquals("hello", response.getLastMessage()); + verify(conversationRepository).save(conversation); + } + + @Test + void closeConversationRejectsOtherCustomer() { + Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); + + assertThrows(AccessDeniedException.class, () -> chatService.closeConversation(99L, 10L, User.Role.CUSTOMER)); + } + + @Test + void sendMessageRejectsClosedConversation() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + + assertThrows(AccessDeniedException.class, + () -> chatService.sendMessage(99L, 10L, User.Role.CUSTOMER, new MessageRequest("hello"))); + + verify(messageRepository, never()).save(any()); + } + + @Test + void requestHumanTakeoverRejectsClosedConversation() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + + assertThrows(AccessDeniedException.class, + () -> chatService.requestHumanTakeover(99L, 10L, User.Role.CUSTOMER)); + } + + private Conversation conversation(Long id, Long customerId, Long staffId, Conversation.ConversationStatus status) { + Conversation conversation = new Conversation(); + conversation.setId(id); + conversation.setCustomerId(customerId); + conversation.setStaffId(staffId); + conversation.setStatus(status); + conversation.setMode(Conversation.ConversationMode.AUTOMATED); + conversation.setHumanRequestedAt(LocalDateTime.now()); + return conversation; + } + + private Message message(String content) { + Message message = new Message(); + message.setConversationId(99L); + message.setSenderId(10L); + message.setContent(content); + message.setIsRead(false); + return message; + } +} From 3b84eff536e8dd732ec346a62f5f9b2e9757e940 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 19:02:19 -0600 Subject: [PATCH 004/137] Fix appointment overlap rules --- .../service/AppointmentServiceTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java new file mode 100644 index 00000000..fbc4ba4b --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -0,0 +1,112 @@ +package com.petshop.backend.service; + +import com.petshop.backend.entity.Appointment; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Service; +import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.ServiceRepository; +import com.petshop.backend.repository.StoreRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AppointmentServiceTest { + + @Mock + private AppointmentRepository appointmentRepository; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private ServiceRepository serviceRepository; + + @Mock + private StoreRepository storeRepository; + + @InjectMocks + private AppointmentService appointmentService; + + private Customer customer; + private StoreLocation store; + private Service grooming; + private Service nailTrim; + private LocalDate date; + + @BeforeEach + void setUp() { + customer = new Customer(); + customer.setCustomerId(1L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + + store = new StoreLocation(); + store.setStoreId(1L); + store.setStoreName("Main Store"); + + grooming = new Service(); + grooming.setServiceId(1L); + grooming.setServiceName("Grooming"); + grooming.setServiceDuration(30); + + nailTrim = new Service(); + nailTrim.setServiceId(2L); + nailTrim.setServiceName("Nail Trim"); + nailTrim.setServiceDuration(30); + + date = LocalDate.now().plusDays(1); + } + + @Test + void checkAvailabilityAllowsDifferentServicesAtSameTime() { + Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); + + List slots = appointmentService.checkAvailability(1L, 2L, date); + + assertTrue(slots.contains("10:00")); + } + + @Test + void checkAvailabilityBlocksSameServiceAtSameTime() { + Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); + + List slots = appointmentService.checkAvailability(1L, 1L, date); + + assertFalse(slots.contains("10:00")); + } + + private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { + Appointment appointment = new Appointment(); + appointment.setAppointmentId(id); + appointment.setAppointmentDate(date); + appointment.setAppointmentTime(time); + appointment.setAppointmentStatus("Booked"); + appointment.setService(service); + appointment.setStore(storeLocation); + appointment.setCustomer(customer); + appointment.setPets(Set.of()); + return appointment; + } +} From 36ac309442ecf39b463fc8eb8245a837f6fed7bd Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:07:10 -0600 Subject: [PATCH 005/137] Update chat conversation status --- .../backend/controller/ChatController.java | 7 +- .../dto/chat/UpdateConversationRequest.java | 25 ++++++ .../petshop/backend/service/ChatService.java | 10 ++- .../backend/service/ChatServiceTest.java | 83 +++++++++++++++++-- 4 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/dto/chat/UpdateConversationRequest.java diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index 59b4f74f..7320cdb9 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest; import com.petshop.backend.dto.chat.ConversationResponse; import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.User; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; @@ -97,11 +98,11 @@ public class ChatController { return ResponseEntity.ok(conversation); } - @PostMapping("/conversations/{id}/close") + @PutMapping("/conversations/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") - public ResponseEntity closeConversation(@PathVariable Long id) { + public ResponseEntity updateConversation(@PathVariable Long id, @Valid @RequestBody UpdateConversationRequest request) { User user = getCurrentUser(); - ConversationResponse conversation = chatService.closeConversation(id, user.getId(), user.getRole()); + ConversationResponse conversation = chatService.updateConversation(id, user.getId(), user.getRole(), request); chatRealtimeService.publishConversationUpdate(id); return ResponseEntity.ok(conversation); } diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/UpdateConversationRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/UpdateConversationRequest.java new file mode 100644 index 00000000..4c043a79 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/chat/UpdateConversationRequest.java @@ -0,0 +1,25 @@ +package com.petshop.backend.dto.chat; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public class UpdateConversationRequest { + @NotBlank(message = "Status is required") + @Pattern(regexp = "^(OPEN|CLOSED)$", message = "Status must be OPEN or CLOSED") + private String status; + + public UpdateConversationRequest() { + } + + public UpdateConversationRequest(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index 08c94478..d9165c9d 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -4,6 +4,7 @@ import com.petshop.backend.dto.chat.ConversationRequest; import com.petshop.backend.dto.chat.ConversationResponse; import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; +import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.Conversation; import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Message; @@ -172,7 +173,7 @@ public class ChatService { } @Transactional - public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) { + public ConversationResponse updateConversation(Long conversationId, Long userId, User.Role role, UpdateConversationRequest request) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); @@ -185,7 +186,7 @@ public class ChatService { } } - conversation.setStatus(Conversation.ConversationStatus.CLOSED); + conversation.setStatus(Conversation.ConversationStatus.valueOf(request.getStatus())); conversation = conversationRepository.save(conversation); List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); @@ -193,6 +194,11 @@ public class ChatService { return ConversationResponse.fromEntity(conversation, lastMessage); } + @Transactional + public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) { + return updateConversation(conversationId, userId, role, new UpdateConversationRequest("CLOSED")); + } + public List getMessages(Long conversationId, Long userId, User.Role role) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); diff --git a/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java b/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java index 323d4c04..4ce13bd2 100644 --- a/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java @@ -1,7 +1,7 @@ package com.petshop.backend.service; -import com.petshop.backend.dto.chat.ConversationRequest; import com.petshop.backend.dto.chat.MessageRequest; +import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.Conversation; import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Message; @@ -60,7 +60,7 @@ class ChatServiceTest { } @Test - void closeConversationMarksConversationClosed() { + void updateConversationMarksConversationClosed() { Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); @@ -68,7 +68,7 @@ class ChatServiceTest { when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)) .thenReturn(List.of(message("hello"))); - var response = chatService.closeConversation(99L, 10L, User.Role.CUSTOMER); + var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")); assertEquals("CLOSED", response.getStatus()); assertEquals("hello", response.getLastMessage()); @@ -76,12 +76,85 @@ class ChatServiceTest { } @Test - void closeConversationRejectsOtherCustomer() { + void updateConversationRejectsOtherCustomer() { Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN); when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - assertThrows(AccessDeniedException.class, () -> chatService.closeConversation(99L, 10L, User.Role.CUSTOMER)); + assertThrows(AccessDeniedException.class, + () -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"))); + } + + @Test + void updateConversationIsIdempotent() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); + + var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")); + + assertEquals("CLOSED", response.getStatus()); + } + + @Test + void staffCanCloseAssignedConversation() { + Conversation conversation = conversation(99L, 1L, 77L, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); + + var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED")); + + assertEquals("CLOSED", response.getStatus()); + } + + @Test + void staffCanCloseUnassignedConversation() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); + + var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED")); + + assertEquals("CLOSED", response.getStatus()); + } + + @Test + void adminCanCloseAnyConversation() { + Conversation conversation = conversation(99L, 2L, 88L, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); + + var response = chatService.updateConversation(99L, 1L, User.Role.ADMIN, new UpdateConversationRequest("CLOSED")); + + assertEquals("CLOSED", response.getStatus()); + } + + @Test + void updateConversationCanReopenClosedConversation() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); + when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); + + var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("OPEN")); + + assertEquals("OPEN", response.getStatus()); + } + + @Test + void updateConversationRejectsInvalidStatus() { + Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); + when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); + when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); + + assertThrows(IllegalArgumentException.class, + () -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("INVALID"))); } @Test From 72b423c8ad1f6f7c38c5a16aa61597ff5f9f1272 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:07:14 -0600 Subject: [PATCH 006/137] Add appointment tests --- .../service/AppointmentServiceTest.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index fbc4ba4b..2a1e6eed 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -2,18 +2,26 @@ package com.petshop.backend.service; import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Service; import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import java.time.LocalDate; import java.time.LocalTime; @@ -21,8 +29,10 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -34,12 +44,18 @@ class AppointmentServiceTest { @Mock private CustomerRepository customerRepository; + @Mock + private PetRepository petRepository; + @Mock private ServiceRepository serviceRepository; @Mock private StoreRepository storeRepository; + @Mock + private UserRepository userRepository; + @InjectMocks private AppointmentService appointmentService; @@ -47,6 +63,7 @@ class AppointmentServiceTest { private StoreLocation store; private Service grooming; private Service nailTrim; + private Pet pet; private LocalDate date; @BeforeEach @@ -70,7 +87,17 @@ class AppointmentServiceTest { nailTrim.setServiceName("Nail Trim"); nailTrim.setServiceDuration(30); + pet = new Pet(); + pet.setPetId(1L); + pet.setPetName("Milo"); + date = LocalDate.now().plusDays(1); + + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); } @Test @@ -97,6 +124,58 @@ class AppointmentServiceTest { assertFalse(slots.contains("10:00")); } + @Test + void cancelledAppointmentsDoNotBlockAvailability() { + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + + List slots = appointmentService.checkAvailability(1L, 1L, date); + + assertTrue(slots.contains("10:00")); + } + + @Test + void updateAppointmentDoesNotConflictWithItself() { + Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); + User user = new User(); + user.setId(10L); + user.setUsername("pat"); + user.setRole(User.Role.CUSTOMER); + user.setTokenVersion(0); + when(userRepository.findById(10L)).thenReturn(Optional.of(user)); + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0), + "n/a", + List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER")) + ) + ); + + when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setPetIds(List.of(1L)); + + var response = appointmentService.updateAppointment(1L, request); + + assertEquals(1L, response.getAppointmentId()); + assertEquals("Booked", response.getAppointmentStatus()); + } + private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { Appointment appointment = new Appointment(); appointment.setAppointmentId(id); From 5d490d7d0544fa404543a0fe43550654301dd354 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:14:53 -0600 Subject: [PATCH 007/137] Remove chat close wrapper --- .../main/java/com/petshop/backend/service/ChatService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index d9165c9d..6ae0c4da 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -194,11 +194,6 @@ public class ChatService { return ConversationResponse.fromEntity(conversation, lastMessage); } - @Transactional - public ConversationResponse closeConversation(Long conversationId, Long userId, User.Role role) { - return updateConversation(conversationId, userId, role, new UpdateConversationRequest("CLOSED")); - } - public List getMessages(Long conversationId, Long userId, User.Role role) { Conversation conversation = conversationRepository.findById(conversationId) .orElseThrow(() -> new ResourceNotFoundException("Conversation not found")); From d8622df3186c4cd377eca3457ff812eac263a928 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:44:06 -0600 Subject: [PATCH 008/137] Backfill user accounts --- .../backend/service/CustomerService.java | 40 ++++++++- .../migration/V9__backfill_user_accounts.sql | 75 ++++++++++++++++ .../backend/service/CustomerServiceTest.java | 85 +++++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql create mode 100644 backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index 040be22a..a34bcb2b 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -4,23 +4,31 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.customer.CustomerRequest; import com.petshop.backend.dto.customer.CustomerResponse; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class CustomerService { + private static final String TEMP_PASSWORD = "TempPass123!"; + private final CustomerRepository customerRepository; private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) { + public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.customerRepository = customerRepository; this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } public Page getAllCustomers(String query, Pageable pageable) { @@ -47,8 +55,13 @@ public class CustomerService { customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - syncLinkedUser(customer); - return mapToResponse(customer); + Customer savedCustomer = customer; + User user = userRepository.findByEmail(savedCustomer.getEmail()) + .orElseGet(() -> createLinkedUser(savedCustomer)); + + Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); + syncLinkedUser(linkedCustomer); + return mapToResponse(linkedCustomer); } @Transactional @@ -99,4 +112,25 @@ public class CustomerService { userRepository.save(user); }); } + + private User createLinkedUser(Customer customer) { + User user = new User(); + user.setUsername(generateUsername(customer)); + user.setPassword(passwordEncoder.encode(TEMP_PASSWORD)); + user.setEmail(customer.getEmail()); + user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); + user.setPhone(generatePhone(customer)); + user.setRole(User.Role.CUSTOMER); + user.setActive(true); + user.setTokenVersion(0); + return userRepository.save(user); + } + + private String generateUsername(Customer customer) { + return "customer_" + customer.getCustomerId(); + } + + private String generatePhone(Customer customer) { + return String.format("200-000-%04d", customer.getCustomerId()); + } } diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql new file mode 100644 index 00000000..9a78abe3 --- /dev/null +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -0,0 +1,75 @@ +INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) +SELECT + CONCAT('customer_', c.customerId) AS username, + '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, + c.email, + CONCAT(c.firstName, ' ', c.lastName) AS fullName, + CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, + 'CUSTOMER' AS role, + TRUE AS active, + 0 AS tokenVersion +FROM customer c +WHERE c.user_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM users u + WHERE u.username = CONCAT('customer_', c.customerId) + OR u.email = c.email + ); + +INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) +SELECT + CONCAT('employee_', e.employeeId) AS username, + '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, + e.email, + CONCAT(e.firstName, ' ', e.lastName) AS fullName, + CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, + CASE + WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' + ELSE 'STAFF' + END AS role, + TRUE AS active, + 0 AS tokenVersion +FROM employee e +WHERE e.user_id IS NULL + AND NOT EXISTS ( + SELECT 1 + FROM users u + WHERE u.username = CONCAT('employee_', e.employeeId) + OR u.email = e.email + ); + +UPDATE customer c +JOIN users u ON u.email = c.email +SET c.user_id = u.id +WHERE c.user_id IS NULL; + +UPDATE employee e +JOIN users u ON u.email = e.email +SET e.user_id = u.id +WHERE e.user_id IS NULL; + +UPDATE users +SET + fullName = CASE + WHEN fullName IS NULL OR fullName = '' THEN username + ELSE fullName + END, + email = CASE + WHEN email IS NULL OR email = '' THEN CONCAT(username, '@petshop.local') + ELSE email + END, + phone = CASE + WHEN phone IS NULL OR phone = '' THEN CONCAT('000-000-', LPAD(id, 4, '0')) + ELSE phone + END, + active = COALESCE(active, TRUE), + tokenVersion = COALESCE(tokenVersion, 0) +WHERE fullName IS NULL + OR fullName = '' + OR email IS NULL + OR email = '' + OR phone IS NULL + OR phone = '' + OR active IS NULL + OR tokenVersion IS NULL; diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java new file mode 100644 index 00000000..b9152511 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -0,0 +1,85 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.customer.CustomerRequest; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CustomerServiceTest { + + @Mock + private CustomerRepository customerRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private UserBusinessLinkageService userBusinessLinkageService; + + @InjectMocks + private CustomerService customerService; + + @Test + void createCustomerCreatesLinkedUser() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + Customer savedCustomer = new Customer(); + savedCustomer.setCustomerId(7L); + savedCustomer.setFirstName("Pat"); + savedCustomer.setLastName("Owner"); + savedCustomer.setEmail("pat@example.com"); + + when(customerRepository.save(any(Customer.class))).thenReturn(savedCustomer); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.empty()); + when(passwordEncoder.encode(any())).thenReturn("hashed-temp-password"); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + user.setId(11L); + return user; + }); + when(userBusinessLinkageService.ensureLinkedCustomer(any(User.class))).thenAnswer(invocation -> { + User user = invocation.getArgument(0); + savedCustomer.setUserId(user.getId()); + return savedCustomer; + }); + + var response = customerService.createCustomer(request); + + assertNotNull(response); + assertEquals("Pat", response.getFirstName()); + assertEquals("Owner", response.getLastName()); + assertEquals("pat@example.com", response.getEmail()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(userRepository).save(userCaptor.capture()); + User createdUser = userCaptor.getValue(); + assertEquals("customer_7", createdUser.getUsername()); + assertEquals("hashed-temp-password", createdUser.getPassword()); + assertEquals("pat@example.com", createdUser.getEmail()); + assertEquals("Pat Owner", createdUser.getFullName()); + assertEquals("200-000-0007", createdUser.getPhone()); + } +} From 0c173060a812c6ec460233bb911e58751f9668d1 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:44:10 -0600 Subject: [PATCH 009/137] Remove debit payment data --- .../petshop/backend/service/SaleService.java | 20 ++++++++++++++++++- .../V10__remove_debit_payment_method.sql | 3 +++ .../resources/db/migration/V2__seed_data.sql | 6 +++--- .../backend/service/SaleServiceTest.java | 17 ++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql create mode 100644 backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b426dc38..b8d5861e 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -78,7 +78,7 @@ public class SaleService { sale.setSaleDate(LocalDateTime.now()); sale.setEmployee(employee); sale.setStore(store); - sale.setPaymentMethod(request.getPaymentMethod()); + sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); if (request.getCustomerId() != null) { @@ -215,4 +215,22 @@ public class SaleService { return response; } + + String normalizePaymentMethod(String paymentMethod) { + if (paymentMethod == null) { + return null; + } + + String normalized = paymentMethod.trim(); + if (normalized.equalsIgnoreCase("Debit")) { + return "Card"; + } + if (normalized.equalsIgnoreCase("Cash")) { + return "Cash"; + } + if (normalized.equalsIgnoreCase("Card")) { + return "Card"; + } + return normalized; + } } diff --git a/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql b/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql new file mode 100644 index 00000000..874b0205 --- /dev/null +++ b/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql @@ -0,0 +1,3 @@ +UPDATE sale +SET paymentMethod = 'Card' +WHERE LOWER(paymentMethod) = 'debit'; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index 5e8d3fb6..d7308ad4 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -128,7 +128,7 @@ VALUES ('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1), ('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2), ('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3), -('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1), +('2026-01-15 10:45:00', 150.00, 'Card', 1, 1, 1), ('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2), ('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL), ('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4), @@ -136,12 +136,12 @@ VALUES ('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1), ('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3), ('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL), -('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2), +('2026-02-08 16:15:00', 160.00, 'Card', 1, 1, 2), ('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL), ('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1), ('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL), ('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4), -('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL), +('2026-02-20 14:35:00', 155.00, 'Card', 4, 3, NULL), ('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2), ('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL), (NOW(), 95.00, 'Card', 1, 1, 1); diff --git a/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java b/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java new file mode 100644 index 00000000..b0ffeded --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java @@ -0,0 +1,17 @@ +package com.petshop.backend.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SaleServiceTest { + + @Test + void normalizePaymentMethodMapsDebitToCard() { + SaleService saleService = new SaleService(null, null, null, null, null, null, null, null); + + assertEquals("Card", saleService.normalizePaymentMethod("Debit")); + assertEquals("Card", saleService.normalizePaymentMethod("debit")); + assertEquals("Cash", saleService.normalizePaymentMethod("Cash")); + } +} From 67f77f4b19584bc95df1b4f44199712aac90d215 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:47:49 -0600 Subject: [PATCH 010/137] added pet images to petfragment and changed other views to look consistant --- .../activities/HomeActivity.java | 7 + .../activities/MainActivity.java | 7 + .../adapters/BlackTextArrayAdapter.java | 44 ++++++ .../adapters/InventoryAdapter.java | 6 +- .../petstoremobile/adapters/PetAdapter.java | 19 +++ .../listfragments/InventoryFragment.java | 3 +- .../fragments/listfragments/PetFragment.java | 10 +- .../fragments/listfragments/SaleFragment.java | 20 ++- .../AdoptionDetailFragment.java | 7 +- .../AppointmentDetailFragment.java | 15 +- .../InventoryDetailFragment.java | 3 +- .../detailfragments/PetDetailFragment.java | 15 +- .../ProductDetailFragment.java | 3 +- .../ProductSupplierDetailFragment.java | 5 +- .../detailfragments/RefundDetailFragment.java | 5 +- .../app/src/main/res/layout/activity_home.xml | 5 +- .../main/res/layout/fragment_inventory.xml | 49 +++--- .../app/src/main/res/layout/fragment_pet.xml | 3 +- .../res/layout/fragment_purchase_order.xml | 3 +- .../app/src/main/res/layout/fragment_sale.xml | 33 +++- .../app/src/main/res/layout/item_adoption.xml | 141 ++++++++++-------- .../src/main/res/layout/item_inventory.xml | 75 +++++----- android/app/src/main/res/layout/item_pet.xml | 135 ++++++++++------- .../app/src/main/res/layout/item_product.xml | 115 +++++++------- .../main/res/layout/item_product_supplier.xml | 73 +++++---- .../main/res/layout/item_purchase_order.xml | 124 ++++++++------- android/app/src/main/res/values/colors.xml | 1 + android/app/src/main/res/values/strings.xml | 1 + android/app/src/main/res/values/themes.xml | 7 +- 29 files changed, 569 insertions(+), 365 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 2f6c9722..aea1f427 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -38,9 +38,16 @@ public class HomeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { + EdgeToEdge.enable(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + //get the bottom navbar from the layout bottomNav = findViewById(R.id.bottom_navigation); diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index 8ce61921..94a51ac3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -36,6 +36,7 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { + EdgeToEdge.enable(this); super.onCreate(savedInstanceState); // Check if user is already logged in @@ -54,6 +55,12 @@ public class MainActivity extends AppCompatActivity { setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + //get all controls from layout tvLoginStatus = findViewById(R.id.tvLoginStatus); etUser = findViewById(R.id.etUser); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java new file mode 100644 index 00000000..22af59de --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/BlackTextArrayAdapter.java @@ -0,0 +1,44 @@ +package com.example.petstoremobile.adapters; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.example.petstoremobile.R; +import java.util.List; + +// A class that overrides the arrayAdapter so the text color changes based on theme +public class BlackTextArrayAdapter extends ArrayAdapter { + public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) { + super(context, resource, objects); + } + + public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white)); + if (view instanceof TextView) { + ((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text)); + } + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getDropDownView(position, convertView, parent); + view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white)); + if (view instanceof TextView) { + ((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text)); + } + return view; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 63290ae7..1e455cc6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -63,10 +63,12 @@ public class InventoryAdapter extends RecyclerView.Adapter { // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; + ImageView ivPetProfile; public PetViewHolder(@NonNull View v) { super(v); @@ -38,6 +45,7 @@ public class PetAdapter extends RecyclerView.Adapter { tvPetAge = v.findViewById(R.id.tvPetAge); tvPetPrice = v.findViewById(R.id.tvPetPrice); tvPetStatus = v.findViewById(R.id.tvPetStatus); + ivPetProfile = v.findViewById(R.id.ivPetProfile); } } @@ -74,6 +82,17 @@ public class PetAdapter extends RecyclerView.Adapter { holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); } + // Load pet image using Glide with circle crop + String imageUrl = RetrofitClient.BASE_URL + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); + Glide.with(holder.itemView.getContext()) + .load(imageUrl) + .circleCrop() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(R.drawable.placeholder) + .error(R.drawable.placeholder) + .into(holder.ivPetProfile); + //when a row is clicked, open the detail view holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 6eb27fb4..37e533ee 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.api.InventoryApi; @@ -140,7 +141,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn categoryNames.add(c.getCategoryName()); } - ArrayAdapter spinnerAdapter = new ArrayAdapter<>( + BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>( requireContext(), android.R.layout.simple_spinner_item, categoryNames); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 91257673..4c8effcf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -21,6 +21,7 @@ import android.widget.Spinner; import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.RetrofitClient; @@ -63,7 +64,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupSearch(view); setupStatusFilter(view); setupSwipeRefresh(view); - loadPetData(); //Add button to opens the add dialog @@ -82,6 +82,12 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return view; } + @Override + public void onResume() { + super.onResume(); + loadPetData(); + } + private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchPet); etSearch.addTextChangedListener(new TextWatcher() { @@ -97,7 +103,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void setupStatusFilter(View view) { spinnerStatus = view.findViewById(R.id.spinnerStatus); String[] statuses = {"All Statuses", "Available", "Adopted"}; - ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); + BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerStatus.setAdapter(adapter); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 544cd378..c813aa5a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -11,6 +11,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; +import android.widget.ImageButton; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.fragments.ListFragment; @@ -26,25 +28,37 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis private SaleAdapter adapter; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + private ImageButton btnHamburger; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_sale, container, false); + btnHamburger = view.findViewById(R.id.btnHamburger); + setupRecyclerView(view); loadSaleData(); setupSearch(view); setupSwipeRefresh(view); + // Make the hamburger button open the drawer from listFragment + if (btnHamburger != null) { + btnHamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + } + return view; } private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchSale); etSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override @@ -127,4 +141,4 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index c32edaf0..47733da2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; @@ -61,7 +62,7 @@ public class AdoptionDetailFragment extends Fragment { } private void setupSpinners() { - spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, STATUSES)); } @@ -102,7 +103,7 @@ public class AdoptionDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Pet --"); for (PetDTO p : petList) names.add(p.getPetName()); - spinnerPet.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedPetId != -1) { for (int i = 0; i < petList.size(); i++) { @@ -134,7 +135,7 @@ public class AdoptionDetailFragment extends Fragment { names.add("-- Select Customer --"); for (CustomerDTO c : customerList) names.add(c.getFirstName() + " " + c.getLastName()); - spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedCustomerId != -1) { for (int i = 0; i < customerList.size(); i++) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 573e26e4..eaabc061 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; @@ -72,15 +73,15 @@ public class AppointmentDetailFragment extends Fragment { } private void setupSpinners() { - spinnerStatus.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, STATUSES)); String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = String.format("%02d:00", HOURS[i]); - spinnerHour.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerHour.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, hours)); - spinnerMinute.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerMinute.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"})); } @@ -124,7 +125,7 @@ public class AppointmentDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Pet --"); for (PetDTO p : petList) names.add(p.getPetName()); - spinnerPet.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedPetId != -1) { for (int i = 0; i < petList.size(); i++) { @@ -154,7 +155,7 @@ public class AppointmentDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Service --"); for (ServiceDTO s : serviceList) names.add(s.getServiceName()); - spinnerService.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerService.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedServiceId != -1) { for (int i = 0; i < serviceList.size(); i++) { @@ -185,7 +186,7 @@ public class AppointmentDetailFragment extends Fragment { names.add("-- Select Customer --"); for (CustomerDTO c : customerList) names.add(c.getFirstName() + " " + c.getLastName()); - spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedCustomerId != -1) { for (int i = 0; i < customerList.size(); i++) { @@ -215,7 +216,7 @@ public class AppointmentDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Store --"); for (StoreDTO s : storeList) names.add(s.getStoreName()); - spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerStore.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedStoreId != -1) { for (int i = 0; i < storeList.size(); i++) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 9846bb36..f98351e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -18,6 +18,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.api.RetrofitClient; @@ -94,7 +95,7 @@ public class InventoryDetailFragment extends Fragment { btnBack = view.findViewById(R.id.btnInventoryBack); // Setup dropdown adapter - dropdownAdapter = new ArrayAdapter<>(requireContext(), + dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); etProductSearch.setAdapter(dropdownAdapter); etProductSearch.setThreshold(1); // start showing after 1 character diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 70c19833..c37558a7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -20,6 +20,7 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.PetDTO; @@ -234,19 +235,9 @@ public class PetDetailFragment extends Fragment { //helper function to set up the spinner menu for pet status private void setupSpinner() { - ArrayAdapter adapter = new ArrayAdapter(requireContext(), + BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, - new String[]{"Available", "Adopted"}) { - - //Override the getView method for the spinner to make the text color darker for more readability - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); - return view; - } - }; + new String[]{"Available", "Adopted"}); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerPetStatus.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 29e3473b..8427ab65 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; @@ -74,7 +75,7 @@ public class ProductDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Category --"); for (CategoryDTO c : categoryList) names.add(c.getCategoryName()); - spinnerCategory.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerCategory.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedCategoryId != -1) { for (int i = 0; i < categoryList.size(); i++) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 729ae49d..2bd47432 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; @@ -80,7 +81,7 @@ public class ProductSupplierDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Product --"); for (ProductDTO p : productList) names.add(p.getProdName()); - spinnerProduct.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerProduct.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedProductId != -1) { for (int i = 0; i < productList.size(); i++) { @@ -111,7 +112,7 @@ public class ProductSupplierDetailFragment extends Fragment { List names = new ArrayList<>(); names.add("-- Select Supplier --"); for (SupplierDTO s : supplierList) names.add(s.getSupCompany()); - spinnerSupplier.setAdapter(new ArrayAdapter<>(requireContext(), + spinnerSupplier.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, names)); if (preselectedSupplierId != -1) { for (int i = 0; i < supplierList.size(); i++) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index 55c0561e..41b8f3b2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -12,6 +12,7 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.utils.ActivityLogger; @@ -126,10 +127,10 @@ public class RefundDetailFragment extends Fragment { } private void setupSpinner() { - ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), + BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, new String[] { "Cash", "Card", "Debit" }); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerRefundPayment.setAdapter(adapter); } -} +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_home.xml b/android/app/src/main/res/layout/activity_home.xml index c7b9f3ea..9c61c329 100644 --- a/android/app/src/main/res/layout/activity_home.xml +++ b/android/app/src/main/res/layout/activity_home.xml @@ -5,13 +5,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:background="@color/background_grey"> + android:background="@color/primary_dark"> + android:layout_weight="1" + android:background="@color/background_grey"/> - - + android:orientation="horizontal" + android:padding="8dp" + android:gravity="center_vertical"> - - + + + + + android:orientation="vertical"> + android:orientation="vertical"> @@ -10,6 +11,33 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + + + + + android:padding="12dp" + android:textColor="@color/text_dark"/> - + \ No newline at end of file diff --git a/android/app/src/main/res/layout/item_adoption.xml b/android/app/src/main/res/layout/item_adoption.xml index 40b03b51..d0ac47f4 100644 --- a/android/app/src/main/res/layout/item_adoption.xml +++ b/android/app/src/main/res/layout/item_adoption.xml @@ -1,78 +1,89 @@ - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:background="@color/white"> + android:orientation="horizontal" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:text="Customer Name" + android:textColor="@color/text_dark" + android:textSize="18sp" + android:textStyle="bold" /> - - - - - - - - - - - - - - - + - \ No newline at end of file + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/item_inventory.xml b/android/app/src/main/res/layout/item_inventory.xml index fed7d903..881aaaea 100644 --- a/android/app/src/main/res/layout/item_inventory.xml +++ b/android/app/src/main/res/layout/item_inventory.xml @@ -6,16 +6,16 @@ android:background="@color/white" android:paddingStart="16dp" android:paddingEnd="16dp" - android:paddingTop="12dp" - android:paddingBottom="12dp"> + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:gravity="center_vertical"> @@ -25,33 +25,50 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - - - + android:gravity="center_vertical"> + android:ellipsize="end" + android:maxLines="1" + android:text="Product Name" + android:textColor="@color/text_dark" + android:textSize="18sp" + android:textStyle="bold" /> + + + + + + + + + android:textSize="13sp" /> - - - + android:layout_marginTop="12dp"/> diff --git a/android/app/src/main/res/layout/item_pet.xml b/android/app/src/main/res/layout/item_pet.xml index d93bf881..d55f64bf 100644 --- a/android/app/src/main/res/layout/item_pet.xml +++ b/android/app/src/main/res/layout/item_pet.xml @@ -1,5 +1,6 @@ - + + + android:orientation="vertical"> - + - + - + - + - + - + + + + + + + + diff --git a/android/app/src/main/res/layout/item_product.xml b/android/app/src/main/res/layout/item_product.xml index 8e96a4d8..fad14129 100644 --- a/android/app/src/main/res/layout/item_product.xml +++ b/android/app/src/main/res/layout/item_product.xml @@ -1,69 +1,70 @@ - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:background="@color/white"> + + + + + + + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginTop="8dp"> - - - - - - - - - - - - - - - + android:layout_weight="1" + android:text="$0.00" + android:textColor="@color/accent_coral" + android:textSize="16sp" + android:textStyle="bold" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/item_product_supplier.xml b/android/app/src/main/res/layout/item_product_supplier.xml index e050966d..00fa3909 100644 --- a/android/app/src/main/res/layout/item_product_supplier.xml +++ b/android/app/src/main/res/layout/item_product_supplier.xml @@ -1,44 +1,59 @@ - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:background="@color/white"> + + + + - - - - + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginTop="8dp"> + android:textSize="16sp" + android:textStyle="bold" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/item_purchase_order.xml b/android/app/src/main/res/layout/item_purchase_order.xml index b3f87cdf..7e9f868f 100644 --- a/android/app/src/main/res/layout/item_purchase_order.xml +++ b/android/app/src/main/res/layout/item_purchase_order.xml @@ -1,70 +1,80 @@ - + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:background="@color/white"> + android:orientation="horizontal" + android:gravity="center_vertical"> - + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:text="PO #000" + android:textColor="@color/text_dark" + android:textSize="18sp" + android:textStyle="bold" /> - - - - - - - - - - - - - + - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 24665e48..0ffc56a3 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -11,4 +11,5 @@ #2ECC71 #E74C3C #3498DB + #000000 \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index ee132b12..478f8011 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ Leons Pet Store Hello blank fragment + Pet Profile Image \ No newline at end of file diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index f92c35db..8ca68229 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -8,10 +8,13 @@ @style/Widget.App.EditText @style/Widget.App.Spinner @color/primary_dark - @color/primary_dark false - false + + + From 909026143d5816727a44aec70b0bc27780e26148 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:52:45 -0600 Subject: [PATCH 011/137] Fix user linking --- .../backend/service/CustomerService.java | 9 ++++++++ .../migration/V9__backfill_user_accounts.sql | 2 ++ .../backend/service/CustomerServiceTest.java | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index a34bcb2b..fb4f75c0 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -13,6 +13,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.CONFLICT; @Service public class CustomerService { @@ -57,6 +60,12 @@ public class CustomerService { customer = customerRepository.save(customer); Customer savedCustomer = customer; User user = userRepository.findByEmail(savedCustomer.getEmail()) + .map(existing -> { + if (existing.getRole() != User.Role.CUSTOMER) { + throw new ResponseStatusException(CONFLICT, "Email already exists for a different account type"); + } + return existing; + }) .orElseGet(() -> createLinkedUser(savedCustomer)); Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql index 9a78abe3..273313a3 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -41,11 +41,13 @@ WHERE e.user_id IS NULL UPDATE customer c JOIN users u ON u.email = c.email + AND u.role = 'CUSTOMER' SET c.user_id = u.id WHERE c.user_id IS NULL; UPDATE employee e JOIN users u ON u.email = e.email + AND u.role IN ('STAFF', 'ADMIN') SET e.user_id = u.id WHERE e.user_id IS NULL; diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index b9152511..6d2bfad2 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -12,10 +12,12 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.server.ResponseStatusException; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -82,4 +84,23 @@ class CustomerServiceTest { assertEquals("Pat Owner", createdUser.getFullName()); assertEquals("200-000-0007", createdUser.getPhone()); } + + @Test + void createCustomerRejectsExistingNonCustomerEmail() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("staff1"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.STAFF); + + when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); + } } From 14ca0d8809537bd2355b2433ad945ba348a3a11e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 21:59:43 -0600 Subject: [PATCH 012/137] Tighten user linking --- .../backend/service/CustomerService.java | 16 +++++++ .../migration/V9__backfill_user_accounts.sql | 12 +++-- .../backend/service/CustomerServiceTest.java | 45 ++++++++++++++++++- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index fb4f75c0..d3c1354d 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -52,6 +52,8 @@ public class CustomerService { @Transactional public CustomerResponse createCustomer(CustomerRequest request) { + ensureEmailAvailable(request.getEmail(), null); + Customer customer = new Customer(); customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); @@ -78,6 +80,8 @@ public class CustomerService { Customer customer = customerRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + ensureEmailAvailable(request.getEmail(), customer.getUserId()); + customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); @@ -142,4 +146,16 @@ public class CustomerService { private String generatePhone(Customer customer) { return String.format("200-000-%04d", customer.getCustomerId()); } + + private void ensureEmailAvailable(String email, Long currentUserId) { + if (email == null || email.isBlank()) { + return; + } + + userRepository.findByEmail(email).ifPresent(existing -> { + if (currentUserId == null || !existing.getId().equals(currentUserId)) { + throw new ResponseStatusException(CONFLICT, "Email already exists"); + } + }); + } } diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql index 273313a3..05cee442 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,7 +2,10 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - c.email, + CASE + WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) THEN CONCAT('customer_', c.customerId, '@petshop.local') + ELSE c.email + END AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -14,14 +17,16 @@ WHERE c.user_id IS NULL SELECT 1 FROM users u WHERE u.username = CONCAT('customer_', c.customerId) - OR u.email = c.email ); INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) SELECT CONCAT('employee_', e.employeeId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - e.email, + CASE + WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) THEN CONCAT('employee_', e.employeeId, '@petshop.local') + ELSE e.email + END AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE @@ -36,7 +41,6 @@ WHERE e.user_id IS NULL SELECT 1 FROM users u WHERE u.username = CONCAT('employee_', e.employeeId) - OR u.email = e.email ); UPDATE customer c diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index 6d2bfad2..4b18a1cd 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -98,9 +98,52 @@ class CustomerServiceTest { existing.setEmail("pat@example.com"); existing.setRole(User.Role.STAFF); - when(customerRepository.save(any(Customer.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); } + + @Test + void createCustomerRejectsExistingCustomerEmail() { + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("customer1"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.CUSTOMER); + + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); + } + + @Test + void updateCustomerRejectsExistingEmailFromOtherUser() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + customer.setEmail("old@example.com"); + + CustomerRequest request = new CustomerRequest(); + request.setFirstName("Pat"); + request.setLastName("Owner"); + request.setEmail("pat@example.com"); + + User existing = new User(); + existing.setId(22L); + existing.setUsername("customer2"); + existing.setEmail("pat@example.com"); + existing.setRole(User.Role.CUSTOMER); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); + + assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request)); + } } From edbaabb42bc6bef9bee92166c360e56b19faf70f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:02:44 -0600 Subject: [PATCH 013/137] Tighten backfill migration --- .../db/migration/V9__backfill_user_accounts.sql | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql index 05cee442..ecb020b1 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,10 +2,7 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) THEN CONCAT('customer_', c.customerId, '@petshop.local') - ELSE c.email - END AS email, + CONCAT('customer_', c.customerId, '@petshop.local') AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -23,10 +20,7 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('employee_', e.employeeId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) THEN CONCAT('employee_', e.employeeId, '@petshop.local') - ELSE e.email - END AS email, + CONCAT('employee_', e.employeeId, '@petshop.local') AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE @@ -44,13 +38,13 @@ WHERE e.user_id IS NULL ); UPDATE customer c -JOIN users u ON u.email = c.email +JOIN users u ON u.username = CONCAT('customer_', c.customerId) AND u.role = 'CUSTOMER' SET c.user_id = u.id WHERE c.user_id IS NULL; UPDATE employee e -JOIN users u ON u.email = e.email +JOIN users u ON u.username = CONCAT('employee_', e.employeeId) AND u.role IN ('STAFF', 'ADMIN') SET e.user_id = u.id WHERE e.user_id IS NULL; From b5efed880d8ac1a75aea85ce2dc24a071c143399 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:09:39 -0600 Subject: [PATCH 014/137] Disable generated user accounts --- .../java/com/petshop/backend/service/CustomerService.java | 2 +- .../resources/db/migration/V9__backfill_user_accounts.sql | 4 ++-- .../java/com/petshop/backend/service/CustomerServiceTest.java | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index d3c1354d..3b1e7dfe 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -134,7 +134,7 @@ public class CustomerService { user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); user.setPhone(generatePhone(customer)); user.setRole(User.Role.CUSTOMER); - user.setActive(true); + user.setActive(false); user.setTokenVersion(0); return userRepository.save(user); } diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql index ecb020b1..1c62e4eb 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -6,7 +6,7 @@ SELECT CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, - TRUE AS active, + FALSE AS active, 0 AS tokenVersion FROM customer c WHERE c.user_id IS NULL @@ -27,7 +27,7 @@ SELECT WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' ELSE 'STAFF' END AS role, - TRUE AS active, + FALSE AS active, 0 AS tokenVersion FROM employee e WHERE e.user_id IS NULL diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index 4b18a1cd..a9d82f5b 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -83,6 +83,7 @@ class CustomerServiceTest { assertEquals("pat@example.com", createdUser.getEmail()); assertEquals("Pat Owner", createdUser.getFullName()); assertEquals("200-000-0007", createdUser.getPhone()); + assertEquals(false, createdUser.getActive()); } @Test From 896f500552bd4aff13a8f632871ea4118508efd8 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:14:53 -0600 Subject: [PATCH 015/137] Preserve backfill emails --- .../migration/V9__backfill_user_accounts.sql | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql index 1c62e4eb..4af69669 100644 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql @@ -2,7 +2,15 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('customer_', c.customerId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CONCAT('customer_', c.customerId, '@petshop.local') AS email, + CASE + WHEN c.email IS NOT NULL + AND c.email <> '' + AND (SELECT COUNT(*) FROM customer c2 WHERE c2.email = c.email) = 1 + AND NOT EXISTS (SELECT 1 FROM employee e2 WHERE e2.email = c.email) + AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) + THEN c.email + ELSE CONCAT('customer_', c.customerId, '@petshop.local') + END AS email, CONCAT(c.firstName, ' ', c.lastName) AS fullName, CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, 'CUSTOMER' AS role, @@ -20,7 +28,15 @@ INSERT INTO users (username, password, email, fullName, phone, role, active, tok SELECT CONCAT('employee_', e.employeeId) AS username, '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CONCAT('employee_', e.employeeId, '@petshop.local') AS email, + CASE + WHEN e.email IS NOT NULL + AND e.email <> '' + AND (SELECT COUNT(*) FROM employee e2 WHERE e2.email = e.email) = 1 + AND NOT EXISTS (SELECT 1 FROM customer c2 WHERE c2.email = e.email) + AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) + THEN e.email + ELSE CONCAT('employee_', e.employeeId, '@petshop.local') + END AS email, CONCAT(e.firstName, ' ', e.lastName) AS fullName, CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, CASE From 2cacf1f85247984b735f2926d524b625d45601f4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:37:18 -0600 Subject: [PATCH 016/137] Clean up customer accounts --- .../backend/service/CustomerService.java | 19 +++++------- .../backend/service/CustomerServiceTest.java | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index 3b1e7dfe..33c731d1 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -60,15 +60,7 @@ public class CustomerService { customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - Customer savedCustomer = customer; - User user = userRepository.findByEmail(savedCustomer.getEmail()) - .map(existing -> { - if (existing.getRole() != User.Role.CUSTOMER) { - throw new ResponseStatusException(CONFLICT, "Email already exists for a different account type"); - } - return existing; - }) - .orElseGet(() -> createLinkedUser(savedCustomer)); + User user = createLinkedUser(customer); Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); syncLinkedUser(linkedCustomer); @@ -93,9 +85,14 @@ public class CustomerService { @Transactional public void deleteCustomer(Long id) { - if (!customerRepository.existsById(id)) { - throw new ResourceNotFoundException("Customer not found with id: " + id); + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + + if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) { + userRepository.deleteById(customer.getUserId()); + return; } + customerRepository.deleteById(id); } diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java index a9d82f5b..dde07768 100644 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -147,4 +148,33 @@ class CustomerServiceTest { assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request)); } + + @Test + void deleteCustomerDeletesLinkedUser() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.existsById(11L)).thenReturn(true); + + customerService.deleteCustomer(7L); + + verify(userRepository).deleteById(11L); + verify(customerRepository, never()).deleteById(7L); + } + + @Test + void deleteCustomerDeletesCustomerWhenNoLinkedUserExists() { + Customer customer = new Customer(); + customer.setCustomerId(7L); + customer.setUserId(11L); + + when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); + when(userRepository.existsById(11L)).thenReturn(false); + + customerService.deleteCustomer(7L); + + verify(customerRepository).deleteById(7L); + } } From 01550bac30260179939a6cb6a46d4245c8d9596f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:54:16 -0600 Subject: [PATCH 017/137] Expand pet product data --- .../V11__expand_pet_product_seed.sql | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql diff --git a/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql b/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql new file mode 100644 index 00000000..f2ca3259 --- /dev/null +++ b/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql @@ -0,0 +1,389 @@ +-- Expand pet and product seed data + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice) +VALUES +('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00), +('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00), +('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00), +('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00), +('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00), +('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00), +('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00), +('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00), +('Leo', 'Dog', 'Husky', 4, 'Available', 749.00), +('Penny', 'Dog', 'Border Collie', 5, 'Available', 808.00), +('Jax', 'Dog', 'German Shepherd', 1, 'Available', 823.00), +('Nala', 'Dog', 'Poodle', 2, 'Available', 871.00), +('Finn', 'Dog', 'Bulldog', 3, 'Available', 447.00), +('Sadie', 'Dog', 'Boxer', 4, 'Available', 495.00), +('Ace', 'Dog', 'Dachshund', 5, 'Available', 510.00), +('Zoe', 'Dog', 'Shih Tzu', 1, 'Available', 547.00), +('Ollie', 'Dog', 'Rottweiler', 2, 'Available', 606.00), +('Millie', 'Dog', 'Corgi', 3, 'Available', 654.00), +('Murphy', 'Dog', 'Husky', 4, 'Available', 691.00), +('Willow', 'Dog', 'Border Collie', 5, 'Available', 728.00), +('Bentley', 'Dog', 'German Shepherd', 1, 'Available', 776.00), +('Lily', 'Dog', 'Poodle', 2, 'Available', 780.00), +('Scout', 'Dog', 'Bulldog', 3, 'Available', 828.00), +('Gracie', 'Dog', 'Boxer', 4, 'Available', 876.00), +('Ranger', 'Dog', 'Dachshund', 5, 'Available', 452.00), +('Hazel', 'Dog', 'Shih Tzu', 1, 'Available', 478.00), +('Moose', 'Dog', 'Rottweiler', 2, 'Available', 515.00), +('Mia', 'Dog', 'Corgi', 3, 'Available', 530.00), +('Simba', 'Cat', 'Ragdoll', 1, 'Available', 295.00), +('Cleo', 'Cat', 'Bengal', 2, 'Available', 321.00), +('Oreo', 'Cat', 'British Shorthair', 3, 'Available', 358.00), +('Pepper', 'Cat', 'Sphynx', 4, 'Available', 417.00), +('Jasper', 'Cat', 'Scottish Fold', 5, 'Available', 454.00), +('Phoebe', 'Cat', 'Russian Blue', 1, 'Available', 491.00), +('Shadow', 'Cat', 'Abyssinian', 2, 'Available', 528.00), +('Mochi', 'Cat', 'Birman', 3, 'Available', 554.00), +('Louie', 'Cat', 'Ragdoll', 4, 'Available', 591.00), +('Ivy', 'Cat', 'Bengal', 5, 'Available', 606.00), +('Theo', 'Cat', 'British Shorthair', 1, 'Available', 654.00), +('Piper', 'Cat', 'Sphynx', 2, 'Available', 251.00), +('Nova', 'Cat', 'Scottish Fold', 3, 'Available', 277.00), +('Archie', 'Cat', 'Russian Blue', 4, 'Available', 336.00), +('Olive', 'Cat', 'Abyssinian', 5, 'Available', 362.00), +('Boots', 'Cat', 'Birman', 1, 'Available', 399.00), +('Maple', 'Cat', 'Ragdoll', 2, 'Available', 436.00), +('Gizmo', 'Cat', 'Bengal', 3, 'Available', 473.00), +('Nina', 'Cat', 'British Shorthair', 4, 'Available', 499.00), +('Salem', 'Cat', 'Sphynx', 5, 'Available', 547.00), +('Stella', 'Cat', 'Scottish Fold', 1, 'Available', 595.00), +('Kiki', 'Cat', 'Russian Blue', 2, 'Available', 610.00), +('Sunny', 'Cat', 'Abyssinian', 3, 'Available', 658.00), +('Mabel', 'Cat', 'Birman', 4, 'Available', 244.00), +('Coco', 'Bird', 'Cockatiel', 1, 'Available', 119.00), +('Sky', 'Bird', 'Parakeet', 2, 'Available', 145.00), +('Sunny', 'Bird', 'Canary', 3, 'Available', 204.00), +('Kiwi', 'Bird', 'Lovebird', 1, 'Available', 230.00), +('Pico', 'Bird', 'Finch', 2, 'Available', 81.00), +('Blue', 'Bird', 'Conure', 3, 'Available', 118.00), +('Rio', 'Bird', 'Cockatiel', 1, 'Available', 144.00), +('Angel', 'Bird', 'Parakeet', 2, 'Available', 203.00), +('Chirpy', 'Bird', 'Canary', 3, 'Available', 251.00), +('Peach', 'Bird', 'Lovebird', 1, 'Available', 91.00), +('Mango', 'Bird', 'Finch', 2, 'Available', 128.00), +('Pearl', 'Bird', 'Conure', 3, 'Available', 165.00), +('Bubbles', 'Fish', 'Goldfish', 1, 'Available', 30.00), +('Splash', 'Fish', 'Betta', 2, 'Available', 56.00), +('Coral', 'Fish', 'Guppy', 1, 'Available', 23.00), +('Neptune', 'Fish', 'Molly', 2, 'Available', 23.00), +('Marlin', 'Fish', 'Tetra', 1, 'Available', 49.00), +('Finley', 'Fish', 'Angelfish', 2, 'Available', 27.00), +('Pebble', 'Fish', 'Goldfish', 1, 'Available', 64.00), +('Wave', 'Fish', 'Betta', 2, 'Available', 20.00), +('Aqua', 'Fish', 'Guppy', 1, 'Available', 57.00), +('Flash', 'Fish', 'Molly', 2, 'Available', 46.00), +('Nemo', 'Fish', 'Tetra', 1, 'Available', 13.00), +('Pearl', 'Fish', 'Angelfish', 2, 'Available', 61.00), +('Thumper', 'Rabbit', 'Mini Lop', 1, 'Available', 147.00), +('Clover', 'Rabbit', 'Netherland Dwarf', 2, 'Available', 173.00), +('Biscuit', 'Rabbit', 'Lionhead', 3, 'Adopted', 110.00), +('Hazel', 'Rabbit', 'Rex', 1, 'Adopted', 125.00), +('Juniper', 'Rabbit', 'Mini Lop', 2, 'Adopted', 73.00), +('Poppy', 'Rabbit', 'Netherland Dwarf', 3, 'Adopted', 88.00), +('Snowball', 'Rabbit', 'Lionhead', 1, 'Adopted', 158.00), +('Maple', 'Rabbit', 'Rex', 2, 'Adopted', 162.00), +('Peanut', 'Hamster', 'Syrian', 1, 'Adopted', 36.00), +('Nibbles', 'Hamster', 'Dwarf', 2, 'Adopted', 36.00), +('Pumpkin', 'Hamster', 'Roborovski', 1, 'Adopted', 25.00), +('Mocha', 'Hamster', 'Syrian', 2, 'Adopted', 40.00), +('Buttons', 'Hamster', 'Dwarf', 1, 'Pending', 51.00), +('Teddy', 'Hamster', 'Roborovski', 2, 'Pending', 18.00), +('Pip', 'Hamster', 'Syrian', 1, 'Pending', 33.00), +('Toffee', 'Hamster', 'Dwarf', 2, 'Pending', 55.00), +('Sprout', 'Hamster', 'Roborovski', 1, 'Pending', 44.00), +('Bean', 'Hamster', 'Syrian', 2, 'Pending', 59.00); + +INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) +VALUES +('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'), +('Beef Feast Dog Food', 51.00, 1, 'Nutritious food and treats for dogs'), +('Salmon Blend Dog Food', 66.00, 1, 'Nutritious food and treats for dogs'), +('Lamb Dinner Dog Food', 78.00, 1, 'Nutritious food and treats for dogs'), +('Puppy Starter Kibble', 24.00, 1, 'Nutritious food and treats for dogs'), +('Senior Care Dog Food', 37.00, 1, 'Nutritious food and treats for dogs'), +('Small Breed Kibble', 48.00, 1, 'Nutritious food and treats for dogs'), +('Large Breed Kibble', 61.00, 1, 'Nutritious food and treats for dogs'), +('Grain Free Dog Food', 75.00, 1, 'Nutritious food and treats for dogs'), +('Turkey Rice Formula', 21.00, 1, 'Nutritious food and treats for dogs'), +('Duck Sweet Potato Meal', 37.00, 1, 'Nutritious food and treats for dogs'), +('Venison Protein Blend', 49.00, 1, 'Nutritious food and treats for dogs'), +('Healthy Weight Dog Food', 64.00, 1, 'Nutritious food and treats for dogs'), +('Sensitive Stomach Kibble', 78.00, 1, 'Nutritious food and treats for dogs'), +('High Energy Dog Food', 20.00, 1, 'Nutritious food and treats for dogs'), +('Organic Dog Biscuits', 33.00, 1, 'Nutritious food and treats for dogs'), +('Peanut Butter Dog Treats', 50.00, 1, 'Nutritious food and treats for dogs'), +('Dental Chew Sticks', 57.00, 1, 'Nutritious food and treats for dogs'), +('Training Treat Bites', 72.00, 1, 'Nutritious food and treats for dogs'), +('Soft Chicken Treats', 17.00, 1, 'Nutritious food and treats for dogs'), +('Pumpkin Fiber Treats', 31.00, 1, 'Nutritious food and treats for dogs'), +('Joint Support Biscuits', 46.00, 1, 'Nutritious food and treats for dogs'), +('Mini Breed Dinner', 54.00, 1, 'Nutritious food and treats for dogs'), +('Farmhouse Dog Meal', 68.00, 1, 'Nutritious food and treats for dogs'), +('Feather Teaser Wand', 8.00, 2, 'Play items for active cats'), +('Catnip Mouse Toy', 18.00, 2, 'Play items for active cats'), +('Jingle Ball Set', 30.00, 2, 'Play items for active cats'), +('Scratching Post Small', 20.00, 2, 'Play items for active cats'), +('Crinkle Tunnel', 26.00, 2, 'Play items for active cats'), +('Laser Pointer Toy', 13.00, 2, 'Play items for active cats'), +('Plush Fish Toy', 23.00, 2, 'Play items for active cats'), +('Spring Coil Pack', 9.00, 2, 'Play items for active cats'), +('Hanging Door Toy', 22.00, 2, 'Play items for active cats'), +('Interactive Puzzle Toy', 12.00, 2, 'Play items for active cats'), +('Catnip Kicker Toy', 20.00, 2, 'Play items for active cats'), +('Rolling Bell Ball', 4.00, 2, 'Play items for active cats'), +('Ribbon Chase Toy', 16.00, 2, 'Play items for active cats'), +('Mini Plush Mouse', 29.00, 2, 'Play items for active cats'), +('Treat Dispensing Ball', 18.00, 2, 'Play items for active cats'), +('Double Pom Toy', 24.00, 2, 'Play items for active cats'), +('Window Perch Toy', 10.00, 2, 'Play items for active cats'), +('Scratch Pad Refill', 25.00, 2, 'Play items for active cats'), +('Rainbow Wand Toy', 7.00, 2, 'Play items for active cats'), +('Carpet Scratcher', 20.00, 2, 'Play items for active cats'), +('Bird Perch Set', 41.00, 3, 'Care supplies for pet birds'), +('Parakeet Seed Mix', 57.00, 3, 'Care supplies for pet birds'), +('Canary Food Blend', 70.00, 3, 'Care supplies for pet birds'), +('Mineral Cuttlebone', 84.00, 3, 'Care supplies for pet birds'), +('Bird Ladder Toy', 94.00, 3, 'Care supplies for pet birds'), +('Mirror Bell Combo', 109.00, 3, 'Care supplies for pet birds'), +('Clip On Food Cup', 121.00, 3, 'Care supplies for pet birds'), +('Bird Cage Liner Pack', 18.00, 3, 'Care supplies for pet birds'), +('Nesting Material Pack', 32.00, 3, 'Care supplies for pet birds'), +('Treat Spray Millet', 42.00, 3, 'Care supplies for pet birds'), +('Wooden Swing Perch', 55.00, 3, 'Care supplies for pet birds'), +('Foraging Ball Toy', 67.00, 3, 'Care supplies for pet birds'), +('Cage Cleaning Spray', 82.00, 3, 'Care supplies for pet birds'), +('Parrot Rope Perch', 93.00, 3, 'Care supplies for pet birds'), +('Bird Bath Dish', 103.00, 3, 'Care supplies for pet birds'), +('Songbird Vitamin Drops', 124.00, 3, 'Care supplies for pet birds'), +('Aquarium Filter Cartridge', 58.00, 4, 'Essential aquarium equipment and accessories'), +('Decorative Aquarium Gravel', 72.00, 4, 'Essential aquarium equipment and accessories'), +('Fish Net Medium', 74.00, 4, 'Essential aquarium equipment and accessories'), +('Water Conditioner', 89.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Thermometer', 105.00, 4, 'Essential aquarium equipment and accessories'), +('LED Tank Light', 112.00, 4, 'Essential aquarium equipment and accessories'), +('Air Stone Pack', 125.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Heater 50W', 143.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Heater 100W', 157.00, 4, 'Essential aquarium equipment and accessories'), +('Fish Flake Food', 165.00, 4, 'Essential aquarium equipment and accessories'), +('Algae Scraper', 176.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Plant Set', 194.00, 4, 'Essential aquarium equipment and accessories'), +('Bubble Curtain Kit', 207.00, 4, 'Essential aquarium equipment and accessories'), +('Breeder Box Insert', 14.00, 4, 'Essential aquarium equipment and accessories'), +('Filter Sponge Pack', 27.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Background Roll', 46.00, 4, 'Essential aquarium equipment and accessories'), +('Glass Lid Clips', 50.00, 4, 'Essential aquarium equipment and accessories'), +('Submersible Pump', 64.00, 4, 'Essential aquarium equipment and accessories'), +('Hamster Bedding Pack', 60.00, 5, 'Supplies for small pets'), +('Rabbit Hay Bundle', 6.00, 5, 'Supplies for small pets'), +('Guinea Pig Pellets', 20.00, 5, 'Supplies for small pets'), +('Small Pet Water Bottle', 37.00, 5, 'Supplies for small pets'), +('Hamster Hideout Hut', 47.00, 5, 'Supplies for small pets'), +('Chew Stick Bundle', 58.00, 5, 'Supplies for small pets'), +('Rabbit Litter Tray', 8.00, 5, 'Supplies for small pets'), +('Exercise Ball Large', 22.00, 5, 'Supplies for small pets'), +('Small Pet Food Bowl', 35.00, 5, 'Supplies for small pets'), +('Timothy Hay Cubes', 46.00, 5, 'Supplies for small pets'), +('Guinea Pig Tunnel', 59.00, 5, 'Supplies for small pets'), +('Hamster Nesting Fluff', 12.00, 5, 'Supplies for small pets'), +('Rabbit Grooming Brush', 25.00, 5, 'Supplies for small pets'), +('Small Pet Carrier', 34.00, 5, 'Supplies for small pets'), +('Hay Rack Feeder', 45.00, 5, 'Supplies for small pets'), +('Wooden Chew Blocks', 61.00, 5, 'Supplies for small pets'); + +INSERT INTO productSupplier (supId, prodId, cost) +VALUES +(1, 7, 26.04), +(2, 8, 33.15), +(3, 9, 44.88), +(4, 10, 55.38), +(5, 11, 17.76), +(1, 12, 22.94), +(2, 13, 31.20), +(3, 14, 41.48), +(4, 15, 53.25), +(5, 16, 15.54), +(1, 17, 22.94), +(2, 18, 31.85), +(3, 19, 43.52), +(4, 20, 55.38), +(5, 21, 14.80), +(1, 22, 20.46), +(2, 23, 32.50), +(3, 24, 38.76), +(4, 25, 51.12), +(5, 26, 12.58), +(1, 27, 19.22), +(2, 28, 29.90), +(3, 29, 36.72), +(4, 30, 48.28), +(5, 31, 5.92), +(1, 32, 11.16), +(2, 33, 19.50), +(3, 34, 13.60), +(4, 35, 18.46), +(5, 36, 9.62), +(1, 37, 14.26), +(2, 38, 5.85), +(3, 39, 14.96), +(4, 40, 8.52), +(5, 41, 14.80), +(1, 42, 2.48), +(2, 43, 10.40), +(3, 44, 19.72), +(4, 45, 12.78), +(5, 46, 17.76), +(1, 47, 6.20), +(2, 48, 16.25), +(3, 49, 4.76), +(4, 50, 14.20), +(5, 51, 30.34), +(1, 52, 35.34), +(2, 53, 45.50), +(3, 54, 57.12), +(4, 55, 66.74), +(5, 56, 80.66), +(1, 57, 75.02), +(2, 58, 11.70), +(3, 59, 21.76), +(4, 60, 29.82), +(5, 61, 40.70), +(1, 62, 41.54), +(2, 63, 53.30), +(3, 64, 63.24), +(4, 65, 73.13), +(5, 66, 91.76), +(1, 67, 35.96), +(2, 68, 46.80), +(3, 69, 50.32), +(4, 70, 63.19), +(5, 71, 77.70), +(1, 72, 69.44), +(2, 73, 81.25), +(3, 74, 97.24), +(4, 75, 111.47), +(5, 76, 122.10), +(1, 77, 109.12), +(2, 78, 126.10), +(3, 79, 140.76), +(4, 80, 9.94), +(5, 81, 19.98), +(1, 82, 28.52), +(2, 83, 32.50), +(3, 84, 43.52), +(4, 85, 42.60), +(5, 86, 4.44), +(1, 87, 12.40), +(2, 88, 24.05), +(3, 89, 31.96), +(4, 90, 41.18), +(5, 91, 5.92), +(1, 92, 13.64), +(2, 93, 22.75), +(3, 94, 31.28), +(4, 95, 41.89), +(5, 96, 8.88), +(1, 97, 15.50), +(2, 98, 22.10), +(3, 99, 30.60), +(4, 100, 43.31); + +INSERT INTO inventory (prodId, quantity) +VALUES +(7, 120), +(8, 137), +(9, 154), +(10, 171), +(11, 128), +(12, 145), +(13, 162), +(14, 179), +(15, 136), +(16, 153), +(17, 170), +(18, 127), +(19, 144), +(20, 161), +(21, 178), +(22, 135), +(23, 152), +(24, 169), +(25, 126), +(26, 143), +(27, 160), +(28, 177), +(29, 134), +(30, 151), +(31, 228), +(32, 185), +(33, 202), +(34, 219), +(35, 236), +(36, 193), +(37, 210), +(38, 227), +(39, 184), +(40, 201), +(41, 218), +(42, 235), +(43, 192), +(44, 209), +(45, 226), +(46, 183), +(47, 200), +(48, 217), +(49, 234), +(50, 191), +(51, 98), +(52, 115), +(53, 72), +(54, 89), +(55, 106), +(56, 123), +(57, 80), +(58, 97), +(59, 114), +(60, 71), +(61, 88), +(62, 105), +(63, 122), +(64, 79), +(65, 96), +(66, 113), +(67, 45), +(68, 62), +(69, 79), +(70, 96), +(71, 53), +(72, 70), +(73, 87), +(74, 104), +(75, 61), +(76, 78), +(77, 95), +(78, 52), +(79, 69), +(80, 86), +(81, 103), +(82, 60), +(83, 77), +(84, 94), +(85, 101), +(86, 118), +(87, 135), +(88, 152), +(89, 109), +(90, 126), +(91, 143), +(92, 100), +(93, 117), +(94, 134), +(95, 151), +(96, 108), +(97, 125), +(98, 142), +(99, 99), +(100, 116); From d5fdee10d5d219e28689521919bcdd8c653d7ae4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 22:54:25 -0600 Subject: [PATCH 018/137] Add pet product filters --- .../controller/CategoryController.java | 3 +- .../backend/controller/PetController.java | 4 +- .../backend/controller/ProductController.java | 3 +- .../repository/CategoryRepository.java | 6 +-- .../backend/repository/PetRepository.java | 8 ++-- .../backend/repository/ProductRepository.java | 6 +-- .../backend/service/CategoryService.java | 19 ++++---- .../petshop/backend/service/PetService.java | 19 ++++---- .../backend/service/ProductService.java | 19 ++++---- .../petshopdesktop/api/endpoints/PetApi.java | 12 ++++- .../api/endpoints/ProductApi.java | 9 +++- .../controllers/PetController.java | 39 +++++++++++++-- .../controllers/ProductController.java | 47 +++++++++++++++++-- .../petshopdesktop/modelviews/pet-view.fxml | 17 ++++--- .../modelviews/product-view.fxml | 16 ++++--- 15 files changed, 166 insertions(+), 61 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/CategoryController.java b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java index fb938dd9..bbce6ec9 100644 --- a/backend/src/main/java/com/petshop/backend/controller/CategoryController.java +++ b/backend/src/main/java/com/petshop/backend/controller/CategoryController.java @@ -25,8 +25,9 @@ public class CategoryController { @GetMapping public ResponseEntity> getAllCategories( @RequestParam(required = false) String q, + @RequestParam(required = false) String type, Pageable pageable) { - return ResponseEntity.ok(categoryService.getAllCategories(q, pageable)); + return ResponseEntity.ok(categoryService.getAllCategories(q, type, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/controller/PetController.java b/backend/src/main/java/com/petshop/backend/controller/PetController.java index 259b0f89..9fb93f0b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetController.java @@ -25,8 +25,10 @@ public class PetController { @GetMapping public ResponseEntity> getAllPets( @RequestParam(required = false) String q, + @RequestParam(required = false) String species, + @RequestParam(required = false) String status, Pageable pageable) { - return ResponseEntity.ok(petService.getAllPets(q, pageable)); + return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/controller/ProductController.java b/backend/src/main/java/com/petshop/backend/controller/ProductController.java index 6531c72d..418d113a 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ProductController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ProductController.java @@ -25,8 +25,9 @@ public class ProductController { @GetMapping public ResponseEntity> getAllProducts( @RequestParam(required = false) String q, + @RequestParam(required = false) Long categoryId, Pageable pageable) { - return ResponseEntity.ok(productService.getAllProducts(q, pageable)); + return ResponseEntity.ok(productService.getAllProducts(q, categoryId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java index ceb30e53..b1871d1f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CategoryRepository.java @@ -16,7 +16,7 @@ public interface CategoryRepository extends JpaRepository { Optional findByCategoryName(String categoryName); @Query("SELECT c FROM Category c WHERE " + - "LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchCategories(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR LOWER(c.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(c.categoryType) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:type IS NULL OR LOWER(c.categoryType) = LOWER(:type))") + Page searchCategories(@Param("q") String query, @Param("type") String type, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index fa9aa5e7..a474fa8b 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository; public interface PetRepository extends JpaRepository { @Query("SELECT p FROM Pet p WHERE " + - "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchPets(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") + Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java index 94f7fb81..7122e6ea 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ProductRepository.java @@ -12,7 +12,7 @@ import org.springframework.stereotype.Repository; public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE " + - "LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.prodDesc) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchProducts(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:categoryId IS NULL OR p.category.categoryId = :categoryId)") + Page searchProducts(@Param("q") String query, @Param("categoryId") Long categoryId, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java index 3da87dd0..1bf175c7 100644 --- a/backend/src/main/java/com/petshop/backend/service/CategoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -20,14 +20,9 @@ public class CategoryService { this.categoryRepository = categoryRepository; } - public Page getAllCategories(String query, Pageable pageable) { - Page categories; - if (query != null && !query.trim().isEmpty()) { - categories = categoryRepository.searchCategories(query, pageable); - } else { - categories = categoryRepository.findAll(pageable); - } - return categories.map(this::mapToResponse); + public Page getAllCategories(String query, String type, Pageable pageable) { + return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable) + .map(this::mapToResponse); } public CategoryResponse getCategoryById(Long id) { @@ -80,4 +75,12 @@ public class CategoryService { category.getUpdatedAt() ); } + + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } } diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 5c35dfd1..4672ee85 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -33,14 +33,9 @@ public class PetService { this.catalogImageStorageService = catalogImageStorageService; } - public Page getAllPets(String query, Pageable pageable) { - Page pets; - if (query != null && !query.trim().isEmpty()) { - pets = petRepository.searchPets(query, pageable); - } else { - pets = petRepository.findAll(pageable); - } - return pets.map(this::mapToResponse); + public Page getAllPets(String query, String species, String status, Pageable pageable) { + return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable) + .map(this::mapToResponse); } public PetResponse getPetById(Long id) { @@ -182,6 +177,14 @@ public class PetService { return status == null ? "" : status.trim(); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private PetResponse mapToResponse(Pet pet) { return new PetResponse( pet.getPetId(), diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 0473a8eb..8d4b1da2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -32,14 +32,9 @@ public class ProductService { this.catalogImageStorageService = catalogImageStorageService; } - public Page getAllProducts(String query, Pageable pageable) { - Page products; - if (query != null && !query.trim().isEmpty()) { - products = productRepository.searchProducts(query, pageable); - } else { - products = productRepository.findAll(pageable); - } - return products.map(this::mapToResponse); + public Page getAllProducts(String query, Long categoryId, Pageable pageable) { + return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable) + .map(this::mapToResponse); } public ProductResponse getProductById(Long id) { @@ -168,4 +163,12 @@ public class ProductService { public record ImagePayload(Resource resource, MediaType mediaType) { } + + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java index f96372e8..92fa28c9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -24,11 +24,17 @@ public class PetApi { return INSTANCE; } - public List listPets(String query) throws Exception { + public List listPets(String query, String species, String status) throws Exception { String path = "/api/v1/pets?page=0&size=1000"; if (query != null && !query.isEmpty()) { path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); } + if (species != null && !species.isEmpty()) { + path += "&species=" + URLEncoder.encode(species, StandardCharsets.UTF_8); + } + if (status != null && !status.isEmpty()) { + path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8); + } String response = apiClient.getRawResponse(path); PageResponse pageResponse = apiClient.getObjectMapper().readValue( response, @@ -40,6 +46,10 @@ public class PetApi { return pageResponse.getContent(); } + public List listPets(String query) throws Exception { + return listPets(query, null, null); + } + public PetResponse createPet(PetRequest request) throws Exception { return apiClient.post("/api/v1/pets", request, PetResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java index 4b8c89f4..c5ec100d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java @@ -24,11 +24,14 @@ public class ProductApi { return INSTANCE; } - public List listProducts(String query) throws Exception { + public List listProducts(String query, Long categoryId) throws Exception { String path = "/api/v1/products?page=0&size=1000"; if (query != null && !query.isEmpty()) { path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); } + if (categoryId != null) { + path += "&categoryId=" + categoryId; + } String response = apiClient.getRawResponse(path); PageResponse pageResponse = apiClient.getObjectMapper().readValue( response, @@ -40,6 +43,10 @@ public class ProductApi { return pageResponse.getContent(); } + public List listProducts(String query) throws Exception { + return listProducts(query, null); + } + public ProductResponse createProduct(ProductRequest request) throws Exception { return apiClient.post("/api/v1/products", request, ProductResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 88928cb5..7c5d13c8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -64,6 +64,12 @@ public class PetController { @FXML private TableView tvPets; + @FXML + private ComboBox cbSpeciesFilter; + + @FXML + private ComboBox cbStatusFilter; + @FXML private TextField txtSearch; @@ -150,6 +156,12 @@ public class PetController { colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); configureImageColumn(colPetImage); + cbSpeciesFilter.setItems(FXCollections.observableArrayList("All Species", "Dog", "Cat", "Bird", "Fish", "Rabbit", "Hamster")); + cbSpeciesFilter.getSelectionModel().selectFirst(); + + cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending")); + cbStatusFilter.getSelectionModel().selectFirst(); + displayPets(); tvPets.getSelectionModel().selectedItemProperty().addListener( @@ -159,9 +171,12 @@ public class PetController { }); txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { - displayFilteredPet(newValue); + applyFilters(); }); + cbSpeciesFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters()); + cbStatusFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters()); + //EventListener for DELETE key tvPets.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { @@ -173,12 +188,14 @@ public class PetController { } private void displayFilteredPet(String filter) { - if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + String species = selectedSpecies(); + String status = selectedStatus(); + if ((filter == null || filter.isEmpty()) && species == null && status == null){ displayPets(); } else { new Thread(() -> { try { - List pets = PetApi.getInstance().listPets(filter); + List pets = PetApi.getInstance().listPets(filter, species, status); List petList = pets.stream() .map(this::mapToPet) .collect(Collectors.toList()); @@ -203,7 +220,7 @@ public class PetController { private void displayPets() { new Thread(() -> { try { - List pets = PetApi.getInstance().listPets(null); + List pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus()); List petList = pets.stream() .map(this::mapToPet) .collect(Collectors.toList()); @@ -224,6 +241,20 @@ public class PetController { }).start(); } + private void applyFilters() { + displayFilteredPet(txtSearch.getText()); + } + + private String selectedSpecies() { + String value = cbSpeciesFilter.getValue(); + return value == null || value.equals("All Species") ? null : value; + } + + private String selectedStatus() { + String value = cbStatusFilter.getValue(); + return value == null || value.equals("All Statuses") ? null : value; + } + private void openDialog(Pet pet, String mode){ //Get new view FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml")); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index 053e0105..8bb51296 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -16,6 +16,8 @@ import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.DTOs.ProductDTO; import org.example.petshopdesktop.api.dto.product.ProductResponse; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.ProductApi; import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController; import org.example.petshopdesktop.util.ActivityLogger; @@ -62,6 +64,9 @@ public class ProductController { @FXML private TableView tvProducts; + @FXML + private ComboBox cbCategoryFilter; + @FXML private TextField txtSearch; @@ -87,6 +92,7 @@ public class ProductController { colProductCategory.setCellValueFactory(new PropertyValueFactory("categoryName")); colProductDesc.setCellValueFactory(new PropertyValueFactory("prodDesc")); configureImageColumn(colProductImage); + loadCategoryFilter(); displayProduct(); @@ -100,9 +106,11 @@ public class ProductController { //EventListener to search when text is changed on searchbar txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { - displayFilteredProduct(newValue); + applyFilters(); }); + cbCategoryFilter.valueProperty().addListener((observable, oldValue, newValue) -> applyFilters()); + //EventListener for DELETE key press tvProducts.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { @@ -120,7 +128,7 @@ public class ProductController { private void displayProduct(){ new Thread(() -> { try { - List products = ProductApi.getInstance().listProducts(null); + List products = ProductApi.getInstance().listProducts(null, selectedCategoryId()); List productDTOs = products.stream() .map(this::mapToProductDTO) .collect(Collectors.toList()); @@ -222,12 +230,12 @@ public class ProductController { * @param filter word to filter table */ private void displayFilteredProduct(String filter){ - if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + if ((txtSearch.getText() == null || txtSearch.getText().isEmpty()) && selectedCategoryId() == null){ displayProduct(); } else { new Thread(() -> { try { - List products = ProductApi.getInstance().listProducts(filter); + List products = ProductApi.getInstance().listProducts(filter, selectedCategoryId()); List productDTOs = products.stream() .map(this::mapToProductDTO) .collect(Collectors.toList()); @@ -249,6 +257,37 @@ public class ProductController { } } + private void applyFilters() { + displayFilteredProduct(txtSearch.getText()); + } + + private void loadCategoryFilter() { + new Thread(() -> { + try { + List options = new ArrayList<>(); + DropdownOption all = new DropdownOption(); + all.setId(null); + all.setLabel("All Categories"); + options.add(all); + options.addAll(DropdownApi.getInstance().getCategories()); + Platform.runLater(() -> { + cbCategoryFilter.setItems(FXCollections.observableArrayList(options)); + cbCategoryFilter.getSelectionModel().selectFirst(); + }); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "ProductController.loadCategoryFilter", + e, + "Loading category filter options")); + } + }).start(); + } + + private Long selectedCategoryId() { + DropdownOption option = cbCategoryFilter.getValue(); + return option == null ? null : option.getId(); + } + /** * Function to open the new Dialog for edit or adding * depending on the mode given diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index b4fcc592..c8e77504 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -2,6 +2,7 @@ + @@ -59,13 +60,15 @@ - - - - - - - + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml index b5392be9..eeff74b4 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/product-view.fxml @@ -2,6 +2,7 @@ + @@ -58,13 +59,14 @@ - - - - - - - + + + + + + + + From b18599c280a567e98cc7be51b709a4a28b39d442 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 23:07:16 -0600 Subject: [PATCH 019/137] Tighten seed filters --- .../petshop/backend/DevStackApplication.java | 1 + .../config/LocalCatalogSeedInitializer.java | 37 ++ .../controller/DropdownController.java | 23 ++ .../V11__expand_pet_product_seed.sql | 389 ------------------ .../resources/dev/expand_pet_product_seed.sql | 226 ++++++++++ .../api/endpoints/DropdownApi.java | 16 + .../controllers/PetController.java | 25 +- .../controllers/ProductController.java | 2 +- .../ProductDialogController.java | 2 +- 9 files changed, 328 insertions(+), 393 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/config/LocalCatalogSeedInitializer.java delete mode 100644 backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql create mode 100644 backend/src/main/resources/dev/expand_pet_product_seed.sql diff --git a/backend/src/main/java/com/petshop/backend/DevStackApplication.java b/backend/src/main/java/com/petshop/backend/DevStackApplication.java index 38846aaa..27933d4b 100644 --- a/backend/src/main/java/com/petshop/backend/DevStackApplication.java +++ b/backend/src/main/java/com/petshop/backend/DevStackApplication.java @@ -33,6 +33,7 @@ public class DevStackApplication { docker.ensureDockerAvailable(); docker.startDatabase(); context = new SpringApplicationBuilder(BackendApplication.class) + .profiles("local") .initializers(new FlywayContextInitializer()) .run(args); context.addApplicationListener(event -> { diff --git a/backend/src/main/java/com/petshop/backend/config/LocalCatalogSeedInitializer.java b/backend/src/main/java/com/petshop/backend/config/LocalCatalogSeedInitializer.java new file mode 100644 index 00000000..18e64f05 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/LocalCatalogSeedInitializer.java @@ -0,0 +1,37 @@ +package com.petshop.backend.config; + +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.ProductRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +@Component +@Profile("local") +public class LocalCatalogSeedInitializer implements CommandLineRunner { + + private final DataSource dataSource; + private final PetRepository petRepository; + private final ProductRepository productRepository; + + public LocalCatalogSeedInitializer(DataSource dataSource, PetRepository petRepository, ProductRepository productRepository) { + this.dataSource = dataSource; + this.petRepository = petRepository; + this.productRepository = productRepository; + } + + @Override + public void run(String... args) { + if (petRepository.count() > 6 || productRepository.count() > 6) { + return; + } + + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8", + new ClassPathResource("dev/expand_pet_product_seed.sql")); + populator.execute(dataSource); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 16763b8a..c942eae8 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -82,6 +82,29 @@ public class DropdownController { ); } + @GetMapping("/product-categories") + public ResponseEntity> getProductCategories() { + return ResponseEntity.ok( + categoryRepository.findAll().stream() + .filter(c -> "product".equalsIgnoreCase(c.getCategoryType())) + .map(c -> new DropdownOption(c.getCategoryId(), c.getCategoryName())) + .collect(Collectors.toList()) + ); + } + + @GetMapping("/pet-species") + public ResponseEntity> getPetSpecies() { + return ResponseEntity.ok( + petRepository.findAll().stream() + .map(p -> p.getPetSpecies()) + .filter(species -> species != null && !species.isBlank()) + .distinct() + .sorted(String.CASE_INSENSITIVE_ORDER) + .map(species -> new DropdownOption(null, species)) + .collect(Collectors.toList()) + ); + } + @GetMapping("/stores") public ResponseEntity> getStores() { return ResponseEntity.ok( diff --git a/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql b/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql deleted file mode 100644 index f2ca3259..00000000 --- a/backend/src/main/resources/db/migration/V11__expand_pet_product_seed.sql +++ /dev/null @@ -1,389 +0,0 @@ --- Expand pet and product seed data - -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice) -VALUES -('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00), -('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00), -('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00), -('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00), -('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00), -('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00), -('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00), -('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00), -('Leo', 'Dog', 'Husky', 4, 'Available', 749.00), -('Penny', 'Dog', 'Border Collie', 5, 'Available', 808.00), -('Jax', 'Dog', 'German Shepherd', 1, 'Available', 823.00), -('Nala', 'Dog', 'Poodle', 2, 'Available', 871.00), -('Finn', 'Dog', 'Bulldog', 3, 'Available', 447.00), -('Sadie', 'Dog', 'Boxer', 4, 'Available', 495.00), -('Ace', 'Dog', 'Dachshund', 5, 'Available', 510.00), -('Zoe', 'Dog', 'Shih Tzu', 1, 'Available', 547.00), -('Ollie', 'Dog', 'Rottweiler', 2, 'Available', 606.00), -('Millie', 'Dog', 'Corgi', 3, 'Available', 654.00), -('Murphy', 'Dog', 'Husky', 4, 'Available', 691.00), -('Willow', 'Dog', 'Border Collie', 5, 'Available', 728.00), -('Bentley', 'Dog', 'German Shepherd', 1, 'Available', 776.00), -('Lily', 'Dog', 'Poodle', 2, 'Available', 780.00), -('Scout', 'Dog', 'Bulldog', 3, 'Available', 828.00), -('Gracie', 'Dog', 'Boxer', 4, 'Available', 876.00), -('Ranger', 'Dog', 'Dachshund', 5, 'Available', 452.00), -('Hazel', 'Dog', 'Shih Tzu', 1, 'Available', 478.00), -('Moose', 'Dog', 'Rottweiler', 2, 'Available', 515.00), -('Mia', 'Dog', 'Corgi', 3, 'Available', 530.00), -('Simba', 'Cat', 'Ragdoll', 1, 'Available', 295.00), -('Cleo', 'Cat', 'Bengal', 2, 'Available', 321.00), -('Oreo', 'Cat', 'British Shorthair', 3, 'Available', 358.00), -('Pepper', 'Cat', 'Sphynx', 4, 'Available', 417.00), -('Jasper', 'Cat', 'Scottish Fold', 5, 'Available', 454.00), -('Phoebe', 'Cat', 'Russian Blue', 1, 'Available', 491.00), -('Shadow', 'Cat', 'Abyssinian', 2, 'Available', 528.00), -('Mochi', 'Cat', 'Birman', 3, 'Available', 554.00), -('Louie', 'Cat', 'Ragdoll', 4, 'Available', 591.00), -('Ivy', 'Cat', 'Bengal', 5, 'Available', 606.00), -('Theo', 'Cat', 'British Shorthair', 1, 'Available', 654.00), -('Piper', 'Cat', 'Sphynx', 2, 'Available', 251.00), -('Nova', 'Cat', 'Scottish Fold', 3, 'Available', 277.00), -('Archie', 'Cat', 'Russian Blue', 4, 'Available', 336.00), -('Olive', 'Cat', 'Abyssinian', 5, 'Available', 362.00), -('Boots', 'Cat', 'Birman', 1, 'Available', 399.00), -('Maple', 'Cat', 'Ragdoll', 2, 'Available', 436.00), -('Gizmo', 'Cat', 'Bengal', 3, 'Available', 473.00), -('Nina', 'Cat', 'British Shorthair', 4, 'Available', 499.00), -('Salem', 'Cat', 'Sphynx', 5, 'Available', 547.00), -('Stella', 'Cat', 'Scottish Fold', 1, 'Available', 595.00), -('Kiki', 'Cat', 'Russian Blue', 2, 'Available', 610.00), -('Sunny', 'Cat', 'Abyssinian', 3, 'Available', 658.00), -('Mabel', 'Cat', 'Birman', 4, 'Available', 244.00), -('Coco', 'Bird', 'Cockatiel', 1, 'Available', 119.00), -('Sky', 'Bird', 'Parakeet', 2, 'Available', 145.00), -('Sunny', 'Bird', 'Canary', 3, 'Available', 204.00), -('Kiwi', 'Bird', 'Lovebird', 1, 'Available', 230.00), -('Pico', 'Bird', 'Finch', 2, 'Available', 81.00), -('Blue', 'Bird', 'Conure', 3, 'Available', 118.00), -('Rio', 'Bird', 'Cockatiel', 1, 'Available', 144.00), -('Angel', 'Bird', 'Parakeet', 2, 'Available', 203.00), -('Chirpy', 'Bird', 'Canary', 3, 'Available', 251.00), -('Peach', 'Bird', 'Lovebird', 1, 'Available', 91.00), -('Mango', 'Bird', 'Finch', 2, 'Available', 128.00), -('Pearl', 'Bird', 'Conure', 3, 'Available', 165.00), -('Bubbles', 'Fish', 'Goldfish', 1, 'Available', 30.00), -('Splash', 'Fish', 'Betta', 2, 'Available', 56.00), -('Coral', 'Fish', 'Guppy', 1, 'Available', 23.00), -('Neptune', 'Fish', 'Molly', 2, 'Available', 23.00), -('Marlin', 'Fish', 'Tetra', 1, 'Available', 49.00), -('Finley', 'Fish', 'Angelfish', 2, 'Available', 27.00), -('Pebble', 'Fish', 'Goldfish', 1, 'Available', 64.00), -('Wave', 'Fish', 'Betta', 2, 'Available', 20.00), -('Aqua', 'Fish', 'Guppy', 1, 'Available', 57.00), -('Flash', 'Fish', 'Molly', 2, 'Available', 46.00), -('Nemo', 'Fish', 'Tetra', 1, 'Available', 13.00), -('Pearl', 'Fish', 'Angelfish', 2, 'Available', 61.00), -('Thumper', 'Rabbit', 'Mini Lop', 1, 'Available', 147.00), -('Clover', 'Rabbit', 'Netherland Dwarf', 2, 'Available', 173.00), -('Biscuit', 'Rabbit', 'Lionhead', 3, 'Adopted', 110.00), -('Hazel', 'Rabbit', 'Rex', 1, 'Adopted', 125.00), -('Juniper', 'Rabbit', 'Mini Lop', 2, 'Adopted', 73.00), -('Poppy', 'Rabbit', 'Netherland Dwarf', 3, 'Adopted', 88.00), -('Snowball', 'Rabbit', 'Lionhead', 1, 'Adopted', 158.00), -('Maple', 'Rabbit', 'Rex', 2, 'Adopted', 162.00), -('Peanut', 'Hamster', 'Syrian', 1, 'Adopted', 36.00), -('Nibbles', 'Hamster', 'Dwarf', 2, 'Adopted', 36.00), -('Pumpkin', 'Hamster', 'Roborovski', 1, 'Adopted', 25.00), -('Mocha', 'Hamster', 'Syrian', 2, 'Adopted', 40.00), -('Buttons', 'Hamster', 'Dwarf', 1, 'Pending', 51.00), -('Teddy', 'Hamster', 'Roborovski', 2, 'Pending', 18.00), -('Pip', 'Hamster', 'Syrian', 1, 'Pending', 33.00), -('Toffee', 'Hamster', 'Dwarf', 2, 'Pending', 55.00), -('Sprout', 'Hamster', 'Roborovski', 1, 'Pending', 44.00), -('Bean', 'Hamster', 'Syrian', 2, 'Pending', 59.00); - -INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) -VALUES -('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'), -('Beef Feast Dog Food', 51.00, 1, 'Nutritious food and treats for dogs'), -('Salmon Blend Dog Food', 66.00, 1, 'Nutritious food and treats for dogs'), -('Lamb Dinner Dog Food', 78.00, 1, 'Nutritious food and treats for dogs'), -('Puppy Starter Kibble', 24.00, 1, 'Nutritious food and treats for dogs'), -('Senior Care Dog Food', 37.00, 1, 'Nutritious food and treats for dogs'), -('Small Breed Kibble', 48.00, 1, 'Nutritious food and treats for dogs'), -('Large Breed Kibble', 61.00, 1, 'Nutritious food and treats for dogs'), -('Grain Free Dog Food', 75.00, 1, 'Nutritious food and treats for dogs'), -('Turkey Rice Formula', 21.00, 1, 'Nutritious food and treats for dogs'), -('Duck Sweet Potato Meal', 37.00, 1, 'Nutritious food and treats for dogs'), -('Venison Protein Blend', 49.00, 1, 'Nutritious food and treats for dogs'), -('Healthy Weight Dog Food', 64.00, 1, 'Nutritious food and treats for dogs'), -('Sensitive Stomach Kibble', 78.00, 1, 'Nutritious food and treats for dogs'), -('High Energy Dog Food', 20.00, 1, 'Nutritious food and treats for dogs'), -('Organic Dog Biscuits', 33.00, 1, 'Nutritious food and treats for dogs'), -('Peanut Butter Dog Treats', 50.00, 1, 'Nutritious food and treats for dogs'), -('Dental Chew Sticks', 57.00, 1, 'Nutritious food and treats for dogs'), -('Training Treat Bites', 72.00, 1, 'Nutritious food and treats for dogs'), -('Soft Chicken Treats', 17.00, 1, 'Nutritious food and treats for dogs'), -('Pumpkin Fiber Treats', 31.00, 1, 'Nutritious food and treats for dogs'), -('Joint Support Biscuits', 46.00, 1, 'Nutritious food and treats for dogs'), -('Mini Breed Dinner', 54.00, 1, 'Nutritious food and treats for dogs'), -('Farmhouse Dog Meal', 68.00, 1, 'Nutritious food and treats for dogs'), -('Feather Teaser Wand', 8.00, 2, 'Play items for active cats'), -('Catnip Mouse Toy', 18.00, 2, 'Play items for active cats'), -('Jingle Ball Set', 30.00, 2, 'Play items for active cats'), -('Scratching Post Small', 20.00, 2, 'Play items for active cats'), -('Crinkle Tunnel', 26.00, 2, 'Play items for active cats'), -('Laser Pointer Toy', 13.00, 2, 'Play items for active cats'), -('Plush Fish Toy', 23.00, 2, 'Play items for active cats'), -('Spring Coil Pack', 9.00, 2, 'Play items for active cats'), -('Hanging Door Toy', 22.00, 2, 'Play items for active cats'), -('Interactive Puzzle Toy', 12.00, 2, 'Play items for active cats'), -('Catnip Kicker Toy', 20.00, 2, 'Play items for active cats'), -('Rolling Bell Ball', 4.00, 2, 'Play items for active cats'), -('Ribbon Chase Toy', 16.00, 2, 'Play items for active cats'), -('Mini Plush Mouse', 29.00, 2, 'Play items for active cats'), -('Treat Dispensing Ball', 18.00, 2, 'Play items for active cats'), -('Double Pom Toy', 24.00, 2, 'Play items for active cats'), -('Window Perch Toy', 10.00, 2, 'Play items for active cats'), -('Scratch Pad Refill', 25.00, 2, 'Play items for active cats'), -('Rainbow Wand Toy', 7.00, 2, 'Play items for active cats'), -('Carpet Scratcher', 20.00, 2, 'Play items for active cats'), -('Bird Perch Set', 41.00, 3, 'Care supplies for pet birds'), -('Parakeet Seed Mix', 57.00, 3, 'Care supplies for pet birds'), -('Canary Food Blend', 70.00, 3, 'Care supplies for pet birds'), -('Mineral Cuttlebone', 84.00, 3, 'Care supplies for pet birds'), -('Bird Ladder Toy', 94.00, 3, 'Care supplies for pet birds'), -('Mirror Bell Combo', 109.00, 3, 'Care supplies for pet birds'), -('Clip On Food Cup', 121.00, 3, 'Care supplies for pet birds'), -('Bird Cage Liner Pack', 18.00, 3, 'Care supplies for pet birds'), -('Nesting Material Pack', 32.00, 3, 'Care supplies for pet birds'), -('Treat Spray Millet', 42.00, 3, 'Care supplies for pet birds'), -('Wooden Swing Perch', 55.00, 3, 'Care supplies for pet birds'), -('Foraging Ball Toy', 67.00, 3, 'Care supplies for pet birds'), -('Cage Cleaning Spray', 82.00, 3, 'Care supplies for pet birds'), -('Parrot Rope Perch', 93.00, 3, 'Care supplies for pet birds'), -('Bird Bath Dish', 103.00, 3, 'Care supplies for pet birds'), -('Songbird Vitamin Drops', 124.00, 3, 'Care supplies for pet birds'), -('Aquarium Filter Cartridge', 58.00, 4, 'Essential aquarium equipment and accessories'), -('Decorative Aquarium Gravel', 72.00, 4, 'Essential aquarium equipment and accessories'), -('Fish Net Medium', 74.00, 4, 'Essential aquarium equipment and accessories'), -('Water Conditioner', 89.00, 4, 'Essential aquarium equipment and accessories'), -('Aquarium Thermometer', 105.00, 4, 'Essential aquarium equipment and accessories'), -('LED Tank Light', 112.00, 4, 'Essential aquarium equipment and accessories'), -('Air Stone Pack', 125.00, 4, 'Essential aquarium equipment and accessories'), -('Aquarium Heater 50W', 143.00, 4, 'Essential aquarium equipment and accessories'), -('Aquarium Heater 100W', 157.00, 4, 'Essential aquarium equipment and accessories'), -('Fish Flake Food', 165.00, 4, 'Essential aquarium equipment and accessories'), -('Algae Scraper', 176.00, 4, 'Essential aquarium equipment and accessories'), -('Aquarium Plant Set', 194.00, 4, 'Essential aquarium equipment and accessories'), -('Bubble Curtain Kit', 207.00, 4, 'Essential aquarium equipment and accessories'), -('Breeder Box Insert', 14.00, 4, 'Essential aquarium equipment and accessories'), -('Filter Sponge Pack', 27.00, 4, 'Essential aquarium equipment and accessories'), -('Aquarium Background Roll', 46.00, 4, 'Essential aquarium equipment and accessories'), -('Glass Lid Clips', 50.00, 4, 'Essential aquarium equipment and accessories'), -('Submersible Pump', 64.00, 4, 'Essential aquarium equipment and accessories'), -('Hamster Bedding Pack', 60.00, 5, 'Supplies for small pets'), -('Rabbit Hay Bundle', 6.00, 5, 'Supplies for small pets'), -('Guinea Pig Pellets', 20.00, 5, 'Supplies for small pets'), -('Small Pet Water Bottle', 37.00, 5, 'Supplies for small pets'), -('Hamster Hideout Hut', 47.00, 5, 'Supplies for small pets'), -('Chew Stick Bundle', 58.00, 5, 'Supplies for small pets'), -('Rabbit Litter Tray', 8.00, 5, 'Supplies for small pets'), -('Exercise Ball Large', 22.00, 5, 'Supplies for small pets'), -('Small Pet Food Bowl', 35.00, 5, 'Supplies for small pets'), -('Timothy Hay Cubes', 46.00, 5, 'Supplies for small pets'), -('Guinea Pig Tunnel', 59.00, 5, 'Supplies for small pets'), -('Hamster Nesting Fluff', 12.00, 5, 'Supplies for small pets'), -('Rabbit Grooming Brush', 25.00, 5, 'Supplies for small pets'), -('Small Pet Carrier', 34.00, 5, 'Supplies for small pets'), -('Hay Rack Feeder', 45.00, 5, 'Supplies for small pets'), -('Wooden Chew Blocks', 61.00, 5, 'Supplies for small pets'); - -INSERT INTO productSupplier (supId, prodId, cost) -VALUES -(1, 7, 26.04), -(2, 8, 33.15), -(3, 9, 44.88), -(4, 10, 55.38), -(5, 11, 17.76), -(1, 12, 22.94), -(2, 13, 31.20), -(3, 14, 41.48), -(4, 15, 53.25), -(5, 16, 15.54), -(1, 17, 22.94), -(2, 18, 31.85), -(3, 19, 43.52), -(4, 20, 55.38), -(5, 21, 14.80), -(1, 22, 20.46), -(2, 23, 32.50), -(3, 24, 38.76), -(4, 25, 51.12), -(5, 26, 12.58), -(1, 27, 19.22), -(2, 28, 29.90), -(3, 29, 36.72), -(4, 30, 48.28), -(5, 31, 5.92), -(1, 32, 11.16), -(2, 33, 19.50), -(3, 34, 13.60), -(4, 35, 18.46), -(5, 36, 9.62), -(1, 37, 14.26), -(2, 38, 5.85), -(3, 39, 14.96), -(4, 40, 8.52), -(5, 41, 14.80), -(1, 42, 2.48), -(2, 43, 10.40), -(3, 44, 19.72), -(4, 45, 12.78), -(5, 46, 17.76), -(1, 47, 6.20), -(2, 48, 16.25), -(3, 49, 4.76), -(4, 50, 14.20), -(5, 51, 30.34), -(1, 52, 35.34), -(2, 53, 45.50), -(3, 54, 57.12), -(4, 55, 66.74), -(5, 56, 80.66), -(1, 57, 75.02), -(2, 58, 11.70), -(3, 59, 21.76), -(4, 60, 29.82), -(5, 61, 40.70), -(1, 62, 41.54), -(2, 63, 53.30), -(3, 64, 63.24), -(4, 65, 73.13), -(5, 66, 91.76), -(1, 67, 35.96), -(2, 68, 46.80), -(3, 69, 50.32), -(4, 70, 63.19), -(5, 71, 77.70), -(1, 72, 69.44), -(2, 73, 81.25), -(3, 74, 97.24), -(4, 75, 111.47), -(5, 76, 122.10), -(1, 77, 109.12), -(2, 78, 126.10), -(3, 79, 140.76), -(4, 80, 9.94), -(5, 81, 19.98), -(1, 82, 28.52), -(2, 83, 32.50), -(3, 84, 43.52), -(4, 85, 42.60), -(5, 86, 4.44), -(1, 87, 12.40), -(2, 88, 24.05), -(3, 89, 31.96), -(4, 90, 41.18), -(5, 91, 5.92), -(1, 92, 13.64), -(2, 93, 22.75), -(3, 94, 31.28), -(4, 95, 41.89), -(5, 96, 8.88), -(1, 97, 15.50), -(2, 98, 22.10), -(3, 99, 30.60), -(4, 100, 43.31); - -INSERT INTO inventory (prodId, quantity) -VALUES -(7, 120), -(8, 137), -(9, 154), -(10, 171), -(11, 128), -(12, 145), -(13, 162), -(14, 179), -(15, 136), -(16, 153), -(17, 170), -(18, 127), -(19, 144), -(20, 161), -(21, 178), -(22, 135), -(23, 152), -(24, 169), -(25, 126), -(26, 143), -(27, 160), -(28, 177), -(29, 134), -(30, 151), -(31, 228), -(32, 185), -(33, 202), -(34, 219), -(35, 236), -(36, 193), -(37, 210), -(38, 227), -(39, 184), -(40, 201), -(41, 218), -(42, 235), -(43, 192), -(44, 209), -(45, 226), -(46, 183), -(47, 200), -(48, 217), -(49, 234), -(50, 191), -(51, 98), -(52, 115), -(53, 72), -(54, 89), -(55, 106), -(56, 123), -(57, 80), -(58, 97), -(59, 114), -(60, 71), -(61, 88), -(62, 105), -(63, 122), -(64, 79), -(65, 96), -(66, 113), -(67, 45), -(68, 62), -(69, 79), -(70, 96), -(71, 53), -(72, 70), -(73, 87), -(74, 104), -(75, 61), -(76, 78), -(77, 95), -(78, 52), -(79, 69), -(80, 86), -(81, 103), -(82, 60), -(83, 77), -(84, 94), -(85, 101), -(86, 118), -(87, 135), -(88, 152), -(89, 109), -(90, 126), -(91, 143), -(92, 100), -(93, 117), -(94, 134), -(95, 151), -(96, 108), -(97, 125), -(98, 142), -(99, 99), -(100, 116); diff --git a/backend/src/main/resources/dev/expand_pet_product_seed.sql b/backend/src/main/resources/dev/expand_pet_product_seed.sql new file mode 100644 index 00000000..63736dda --- /dev/null +++ b/backend/src/main/resources/dev/expand_pet_product_seed.sql @@ -0,0 +1,226 @@ +-- Expand pet and product seed data + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice) +VALUES +('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00), +('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00), +('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00), +('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00), +('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00), +('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00), +('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00), +('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00), +('Leo', 'Dog', 'Husky', 4, 'Available', 749.00), +('Penny', 'Dog', 'Border Collie', 5, 'Available', 808.00), +('Jax', 'Dog', 'German Shepherd', 1, 'Available', 823.00), +('Nala', 'Dog', 'Poodle', 2, 'Available', 871.00), +('Finn', 'Dog', 'Bulldog', 3, 'Available', 447.00), +('Sadie', 'Dog', 'Boxer', 4, 'Available', 495.00), +('Ace', 'Dog', 'Dachshund', 5, 'Available', 510.00), +('Zoe', 'Dog', 'Shih Tzu', 1, 'Available', 547.00), +('Ollie', 'Dog', 'Rottweiler', 2, 'Available', 606.00), +('Millie', 'Dog', 'Corgi', 3, 'Available', 654.00), +('Murphy', 'Dog', 'Husky', 4, 'Available', 691.00), +('Willow', 'Dog', 'Border Collie', 5, 'Available', 728.00), +('Bentley', 'Dog', 'German Shepherd', 1, 'Available', 776.00), +('Lily', 'Dog', 'Poodle', 2, 'Available', 780.00), +('Scout', 'Dog', 'Bulldog', 3, 'Available', 828.00), +('Gracie', 'Dog', 'Boxer', 4, 'Available', 876.00), +('Ranger', 'Dog', 'Dachshund', 5, 'Available', 452.00), +('Hazel', 'Dog', 'Shih Tzu', 1, 'Available', 478.00), +('Moose', 'Dog', 'Rottweiler', 2, 'Available', 515.00), +('Mia', 'Dog', 'Corgi', 3, 'Available', 530.00), +('Simba', 'Cat', 'Ragdoll', 1, 'Available', 295.00), +('Cleo', 'Cat', 'Bengal', 2, 'Available', 321.00), +('Oreo', 'Cat', 'British Shorthair', 3, 'Available', 358.00), +('Pepper', 'Cat', 'Sphynx', 4, 'Available', 417.00), +('Jasper', 'Cat', 'Scottish Fold', 5, 'Available', 454.00), +('Phoebe', 'Cat', 'Russian Blue', 1, 'Available', 491.00), +('Shadow', 'Cat', 'Abyssinian', 2, 'Available', 528.00), +('Mochi', 'Cat', 'Birman', 3, 'Available', 554.00), +('Louie', 'Cat', 'Ragdoll', 4, 'Available', 591.00), +('Ivy', 'Cat', 'Bengal', 5, 'Available', 606.00), +('Theo', 'Cat', 'British Shorthair', 1, 'Available', 654.00), +('Piper', 'Cat', 'Sphynx', 2, 'Available', 251.00), +('Nova', 'Cat', 'Scottish Fold', 3, 'Available', 277.00), +('Archie', 'Cat', 'Russian Blue', 4, 'Available', 336.00), +('Olive', 'Cat', 'Abyssinian', 5, 'Available', 362.00), +('Boots', 'Cat', 'Birman', 1, 'Available', 399.00), +('Maple', 'Cat', 'Ragdoll', 2, 'Available', 436.00), +('Gizmo', 'Cat', 'Bengal', 3, 'Available', 473.00), +('Nina', 'Cat', 'British Shorthair', 4, 'Available', 499.00), +('Salem', 'Cat', 'Sphynx', 5, 'Available', 547.00), +('Stella', 'Cat', 'Scottish Fold', 1, 'Available', 595.00), +('Kiki', 'Cat', 'Russian Blue', 2, 'Available', 610.00), +('Sunny', 'Cat', 'Abyssinian', 3, 'Available', 658.00), +('Mabel', 'Cat', 'Birman', 4, 'Available', 244.00), +('Coco', 'Bird', 'Cockatiel', 1, 'Available', 119.00), +('Sky', 'Bird', 'Parakeet', 2, 'Available', 145.00), +('Sunny', 'Bird', 'Canary', 3, 'Available', 204.00), +('Kiwi', 'Bird', 'Lovebird', 1, 'Available', 230.00), +('Pico', 'Bird', 'Finch', 2, 'Available', 81.00), +('Blue', 'Bird', 'Conure', 3, 'Available', 118.00), +('Rio', 'Bird', 'Cockatiel', 1, 'Available', 144.00), +('Angel', 'Bird', 'Parakeet', 2, 'Available', 203.00), +('Chirpy', 'Bird', 'Canary', 3, 'Available', 251.00), +('Peach', 'Bird', 'Lovebird', 1, 'Available', 91.00), +('Mango', 'Bird', 'Finch', 2, 'Available', 128.00), +('Pearl', 'Bird', 'Conure', 3, 'Available', 165.00), +('Bubbles', 'Fish', 'Goldfish', 1, 'Available', 30.00), +('Splash', 'Fish', 'Betta', 2, 'Available', 56.00), +('Coral', 'Fish', 'Guppy', 1, 'Available', 23.00), +('Neptune', 'Fish', 'Molly', 2, 'Available', 23.00), +('Marlin', 'Fish', 'Tetra', 1, 'Available', 49.00), +('Finley', 'Fish', 'Angelfish', 2, 'Available', 27.00), +('Pebble', 'Fish', 'Goldfish', 1, 'Available', 64.00), +('Wave', 'Fish', 'Betta', 2, 'Available', 20.00), +('Aqua', 'Fish', 'Guppy', 1, 'Available', 57.00), +('Flash', 'Fish', 'Molly', 2, 'Available', 46.00), +('Nemo', 'Fish', 'Tetra', 1, 'Available', 13.00), +('Pearl', 'Fish', 'Angelfish', 2, 'Available', 61.00), +('Thumper', 'Rabbit', 'Mini Lop', 1, 'Adopted', 147.00), +('Clover', 'Rabbit', 'Netherland Dwarf', 2, 'Adopted', 138.00), +('Biscuit', 'Rabbit', 'Lionhead', 3, 'Adopted', 177.00), +('Hazel', 'Rabbit', 'Rex', 1, 'Adopted', 91.00), +('Juniper', 'Rabbit', 'Mini Lop', 2, 'Adopted', 83.00), +('Poppy', 'Rabbit', 'Netherland Dwarf', 3, 'Adopted', 111.00), +('Snowball', 'Rabbit', 'Lionhead', 1, 'Adopted', 172.00), +('Maple', 'Rabbit', 'Rex', 2, 'Adopted', 150.00), +('Peanut', 'Hamster', 'Syrian', 1, 'Adopted', 29.00), +('Nibbles', 'Hamster', 'Dwarf', 2, 'Adopted', 42.00), +('Pumpkin', 'Hamster', 'Roborovski', 1, 'Pending', 49.00), +('Mocha', 'Hamster', 'Syrian', 2, 'Pending', 48.00), +('Buttons', 'Hamster', 'Dwarf', 1, 'Pending', 61.00), +('Teddy', 'Hamster', 'Roborovski', 2, 'Pending', 35.00), +('Pip', 'Hamster', 'Syrian', 1, 'Pending', 39.00), +('Toffee', 'Hamster', 'Dwarf', 2, 'Pending', 52.00), +('Sprout', 'Hamster', 'Roborovski', 1, 'Available', 26.00), +('Bean', 'Hamster', 'Syrian', 2, 'Available', 28.00); + +INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) +VALUES +('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'), +('Beef Feast Dog Food', 51.00, 1, 'Nutritious food and treats for dogs'), +('Salmon Blend Dog Food', 17.00, 1, 'Nutritious food and treats for dogs'), +('Lamb Dinner Dog Food', 28.00, 1, 'Nutritious food and treats for dogs'), +('Puppy Starter Kibble', 39.00, 1, 'Nutritious food and treats for dogs'), +('Senior Care Dog Food', 40.00, 1, 'Nutritious food and treats for dogs'), +('Small Breed Kibble', 44.00, 1, 'Nutritious food and treats for dogs'), +('Large Breed Kibble', 57.00, 1, 'Nutritious food and treats for dogs'), +('Grain Free Dog Food', 68.00, 1, 'Nutritious food and treats for dogs'), +('Turkey Rice Formula', 79.00, 1, 'Nutritious food and treats for dogs'), +('Duck Sweet Potato Meal', 25.00, 1, 'Nutritious food and treats for dogs'), +('Venison Protein Blend', 36.00, 1, 'Nutritious food and treats for dogs'), +('Healthy Weight Dog Food', 48.00, 1, 'Nutritious food and treats for dogs'), +('Sensitive Stomach Kibble', 62.00, 1, 'Nutritious food and treats for dogs'), +('High Energy Dog Food', 72.00, 1, 'Nutritious food and treats for dogs'), +('Organic Dog Biscuits', 18.00, 1, 'Nutritious food and treats for dogs'), +('Peanut Butter Dog Treats', 33.00, 1, 'Nutritious food and treats for dogs'), +('Dental Chew Sticks', 38.00, 1, 'Nutritious food and treats for dogs'), +('Training Treat Bites', 48.00, 1, 'Nutritious food and treats for dogs'), +('Soft Chicken Treats', 57.00, 1, 'Nutritious food and treats for dogs'), +('Pumpkin Fiber Treats', 70.00, 1, 'Nutritious food and treats for dogs'), +('Joint Support Biscuits', 14.00, 1, 'Nutritious food and treats for dogs'), +('Mini Breed Dinner', 17.00, 1, 'Nutritious food and treats for dogs'), +('Farmhouse Dog Meal', 30.00, 1, 'Nutritious food and treats for dogs'), +('Feather Teaser Wand', 30.00, 2, 'Play items for active cats'), +('Catnip Mouse Toy', 24.00, 2, 'Play items for active cats'), +('Jingle Ball Set', 18.00, 2, 'Play items for active cats'), +('Scratching Post Small', 6.00, 2, 'Play items for active cats'), +('Crinkle Tunnel', 31.00, 2, 'Play items for active cats'), +('Laser Pointer Toy', 6.00, 2, 'Play items for active cats'), +('Plush Fish Toy', 19.00, 2, 'Play items for active cats'), +('Spring Coil Pack', 20.00, 2, 'Play items for active cats'), +('Hanging Door Toy', 12.00, 2, 'Play items for active cats'), +('Interactive Puzzle Toy', 22.00, 2, 'Play items for active cats'), +('Catnip Kicker Toy', 20.00, 2, 'Play items for active cats'), +('Rolling Bell Ball', 20.00, 2, 'Play items for active cats'), +('Ribbon Chase Toy', 19.00, 2, 'Play items for active cats'), +('Mini Plush Mouse', 21.00, 2, 'Play items for active cats'), +('Treat Dispensing Ball', 16.00, 2, 'Play items for active cats'), +('Double Pom Toy', 12.00, 2, 'Play items for active cats'), +('Window Perch Toy', 10.00, 2, 'Play items for active cats'), +('Scratch Pad Refill', 8.00, 2, 'Play items for active cats'), +('Rainbow Wand Toy', 23.00, 2, 'Play items for active cats'), +('Carpet Scratcher', 23.00, 2, 'Play items for active cats'), +('Bird Perch Set', 27.00, 3, 'Care supplies for pet birds'), +('Parakeet Seed Mix', 40.00, 3, 'Care supplies for pet birds'), +('Canary Food Blend', 53.00, 3, 'Care supplies for pet birds'), +('Mineral Cuttlebone', 57.00, 3, 'Care supplies for pet birds'), +('Bird Ladder Toy', 68.00, 3, 'Care supplies for pet birds'), +('Mirror Bell Combo', 80.00, 3, 'Care supplies for pet birds'), +('Clip On Food Cup', 92.00, 3, 'Care supplies for pet birds'), +('Bird Cage Liner Pack', 108.00, 3, 'Care supplies for pet birds'), +('Nesting Material Pack', 121.00, 3, 'Care supplies for pet birds'), +('Treat Spray Millet', 8.00, 3, 'Care supplies for pet birds'), +('Wooden Swing Perch', 22.00, 3, 'Care supplies for pet birds'), +('Foraging Ball Toy', 32.00, 3, 'Care supplies for pet birds'), +('Cage Cleaning Spray', 47.00, 3, 'Care supplies for pet birds'), +('Parrot Rope Perch', 54.00, 3, 'Care supplies for pet birds'), +('Bird Bath Dish', 54.00, 3, 'Care supplies for pet birds'), +('Songbird Vitamin Drops', 78.00, 3, 'Care supplies for pet birds'), +('Aquarium Filter Cartridge', 36.00, 4, 'Essential aquarium equipment and accessories'), +('Decorative Aquarium Gravel', 49.00, 4, 'Essential aquarium equipment and accessories'), +('Fish Net Medium', 34.00, 4, 'Essential aquarium equipment and accessories'), +('Water Conditioner', 45.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Thermometer', 59.00, 4, 'Essential aquarium equipment and accessories'), +('LED Tank Light', 67.00, 4, 'Essential aquarium equipment and accessories'), +('Air Stone Pack', 76.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Heater 50W', 92.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Heater 100W', 106.00, 4, 'Essential aquarium equipment and accessories'), +('Fish Flake Food', 95.00, 4, 'Essential aquarium equipment and accessories'), +('Algae Scraper', 105.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Plant Set', 122.00, 4, 'Essential aquarium equipment and accessories'), +('Bubble Curtain Kit', 136.00, 4, 'Essential aquarium equipment and accessories'), +('Breeder Box Insert', 149.00, 4, 'Essential aquarium equipment and accessories'), +('Filter Sponge Pack', 164.00, 4, 'Essential aquarium equipment and accessories'), +('Aquarium Background Roll', 183.00, 4, 'Essential aquarium equipment and accessories'), +('Glass Lid Clips', 174.00, 4, 'Essential aquarium equipment and accessories'), +('Submersible Pump', 191.00, 4, 'Essential aquarium equipment and accessories'), +('Hamster Bedding Pack', 50.00, 5, 'Supplies for small pets'), +('Rabbit Hay Bundle', 49.00, 5, 'Supplies for small pets'), +('Guinea Pig Pellets', 15.00, 5, 'Supplies for small pets'), +('Small Pet Water Bottle', 31.00, 5, 'Supplies for small pets'), +('Hamster Hideout Hut', 40.00, 5, 'Supplies for small pets'), +('Chew Stick Bundle', 48.00, 5, 'Supplies for small pets'), +('Rabbit Litter Tray', 58.00, 5, 'Supplies for small pets'), +('Exercise Ball Large', 68.00, 5, 'Supplies for small pets'), +('Small Pet Food Bowl', 20.00, 5, 'Supplies for small pets'), +('Timothy Hay Cubes', 28.00, 5, 'Supplies for small pets'), +('Guinea Pig Tunnel', 38.00, 5, 'Supplies for small pets'), +('Hamster Nesting Fluff', 47.00, 5, 'Supplies for small pets'), +('Rabbit Grooming Brush', 60.00, 5, 'Supplies for small pets'), +('Small Pet Carrier', 7.00, 5, 'Supplies for small pets'), +('Hay Rack Feeder', 11.00, 5, 'Supplies for small pets'), +('Wooden Chew Blocks', 27.00, 5, 'Supplies for small pets'); + +INSERT INTO productSupplier (supId, prodId, cost) +SELECT CASE MOD(p.prodId - 7, 5) + WHEN 0 THEN 1 + WHEN 1 THEN 2 + WHEN 2 THEN 3 + WHEN 3 THEN 4 + ELSE 5 + END, + p.prodId, + ROUND(p.prodPrice * (0.62 + (MOD(p.prodId - 7, 5) * 0.03)), 2) +FROM product p +WHERE p.prodId >= 7 + AND NOT EXISTS ( + SELECT 1 FROM productSupplier ps WHERE ps.prodId = p.prodId + ); + +INSERT INTO inventory (prodId, quantity) +SELECT p.prodId, + CASE p.categoryId + WHEN 1 THEN 120 + MOD((p.prodId - 7) * 17, 60) + WHEN 2 THEN 180 + MOD((p.prodId - 7) * 17, 60) + WHEN 3 THEN 70 + MOD((p.prodId - 7) * 17, 60) + WHEN 4 THEN 45 + MOD((p.prodId - 7) * 17, 60) + ELSE 95 + MOD((p.prodId - 7) * 17, 60) + END +FROM product p +WHERE p.prodId >= 7 + AND NOT EXISTS ( + SELECT 1 FROM inventory i WHERE i.prodId = p.prodId + ); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index 6c20526e..30fcb0b8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -26,6 +26,22 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getProductCategories() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/product-categories"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from product categories endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getPetSpecies() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/pet-species"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from pet species endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getProducts() throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/products"); if (response == null || response.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index 7c5d13c8..bd76c9ce 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -15,6 +15,8 @@ import javafx.scene.layout.StackPane; import javafx.stage.Modality; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.pet.PetResponse; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController; import org.example.petshopdesktop.models.Pet; @@ -156,8 +158,7 @@ public class PetController { colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); configureImageColumn(colPetImage); - cbSpeciesFilter.setItems(FXCollections.observableArrayList("All Species", "Dog", "Cat", "Bird", "Fish", "Rabbit", "Hamster")); - cbSpeciesFilter.getSelectionModel().selectFirst(); + loadSpeciesFilter(); cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending")); cbStatusFilter.getSelectionModel().selectFirst(); @@ -245,6 +246,26 @@ public class PetController { displayFilteredPet(txtSearch.getText()); } + private void loadSpeciesFilter() { + new Thread(() -> { + try { + List values = DropdownApi.getInstance().getPetSpecies().stream() + .map(DropdownOption::getLabel) + .collect(Collectors.toList()); + values.add(0, "All Species"); + Platform.runLater(() -> { + cbSpeciesFilter.setItems(FXCollections.observableArrayList(values)); + cbSpeciesFilter.getSelectionModel().selectFirst(); + }); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "PetController.loadSpeciesFilter", + e, + "Loading species filter options")); + } + }).start(); + } + private String selectedSpecies() { String value = cbSpeciesFilter.getValue(); return value == null || value.equals("All Species") ? null : value; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java index 8bb51296..84cfb8ce 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -269,7 +269,7 @@ public class ProductController { all.setId(null); all.setLabel("All Categories"); options.add(all); - options.addAll(DropdownApi.getInstance().getCategories()); + options.addAll(DropdownApi.getInstance().getProductCategories()); Platform.runLater(() -> { cbCategoryFilter.setItems(FXCollections.observableArrayList(options)); cbCategoryFilter.getSelectionModel().selectFirst(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java index 7b354a9d..2a51e42a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java @@ -89,7 +89,7 @@ public class ProductDialogController { //Set up combobox for selecting category try { - List categories = DropdownApi.getInstance().getCategories(); + List categories = DropdownApi.getInstance().getProductCategories(); if (categories != null) { ObservableList categoriesObs = FXCollections.observableArrayList(categories); cbProdCategory.setItems(categoriesObs); From 9c7f931df2d22bc29853657f3cb544baaf087ab5 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 23:34:43 -0600 Subject: [PATCH 020/137] Scope staff analytics --- .../controller/AnalyticsController.java | 12 +- .../dto/analytics/DashboardResponse.java | 103 +++++++++++++++++- .../backend/service/AnalyticsService.java | 70 +++++++++++- 3 files changed, 171 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java index 6c7b9b9e..002733fa 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -1,26 +1,32 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.analytics.DashboardResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AnalyticsService; +import com.petshop.backend.util.AuthenticationHelper; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/analytics") -@PreAuthorize("hasRole('ADMIN')") +@PreAuthorize("hasAnyRole('ADMIN', 'STAFF')") public class AnalyticsController { private final AnalyticsService analyticsService; + private final UserRepository userRepository; - public AnalyticsController(AnalyticsService analyticsService) { + public AnalyticsController(AnalyticsService analyticsService, UserRepository userRepository) { this.analyticsService = analyticsService; + this.userRepository = userRepository; } @GetMapping("/dashboard") public ResponseEntity getDashboard( @RequestParam(defaultValue = "30") int days, @RequestParam(defaultValue = "10") int top) { - return ResponseEntity.ok(analyticsService.getDashboardData(days, top)); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user)); } } diff --git a/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java index c884d24c..56788f77 100644 --- a/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/analytics/DashboardResponse.java @@ -9,15 +9,19 @@ public class DashboardResponse { private InventorySummary inventorySummary; private List topProducts; private List dailySales; + private List paymentMethods; + private List employeePerformance; public DashboardResponse() { } - public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List topProducts, List dailySales) { + public DashboardResponse(SalesSummary salesSummary, InventorySummary inventorySummary, List topProducts, List dailySales, List paymentMethods, List employeePerformance) { this.salesSummary = salesSummary; this.inventorySummary = inventorySummary; this.topProducts = topProducts; this.dailySales = dailySales; + this.paymentMethods = paymentMethods; + this.employeePerformance = employeePerformance; } public SalesSummary getSalesSummary() { @@ -52,17 +56,33 @@ public class DashboardResponse { this.dailySales = dailySales; } + public List getPaymentMethods() { + return paymentMethods; + } + + public void setPaymentMethods(List paymentMethods) { + this.paymentMethods = paymentMethods; + } + + public List getEmployeePerformance() { + return employeePerformance; + } + + public void setEmployeePerformance(List employeePerformance) { + this.employeePerformance = employeePerformance; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DashboardResponse that = (DashboardResponse) o; - return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales); + return Objects.equals(salesSummary, that.salesSummary) && Objects.equals(inventorySummary, that.inventorySummary) && Objects.equals(topProducts, that.topProducts) && Objects.equals(dailySales, that.dailySales) && Objects.equals(paymentMethods, that.paymentMethods) && Objects.equals(employeePerformance, that.employeePerformance); } @Override public int hashCode() { - return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales); + return Objects.hash(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance); } @Override @@ -72,6 +92,8 @@ public class DashboardResponse { ", inventorySummary=" + inventorySummary + ", topProducts=" + topProducts + ", dailySales=" + dailySales + + ", paymentMethods=" + paymentMethods + + ", employeePerformance=" + employeePerformance + '}'; } @@ -80,15 +102,17 @@ public class DashboardResponse { private Long totalSales; private BigDecimal totalRefunds; private Long totalRefundCount; + private Long totalItemsSold; public SalesSummary() { } - public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount) { + public SalesSummary(BigDecimal totalRevenue, Long totalSales, BigDecimal totalRefunds, Long totalRefundCount, Long totalItemsSold) { this.totalRevenue = totalRevenue; this.totalSales = totalSales; this.totalRefunds = totalRefunds; this.totalRefundCount = totalRefundCount; + this.totalItemsSold = totalItemsSold; } public BigDecimal getTotalRevenue() { @@ -123,17 +147,25 @@ public class DashboardResponse { this.totalRefundCount = totalRefundCount; } + public Long getTotalItemsSold() { + return totalItemsSold; + } + + public void setTotalItemsSold(Long totalItemsSold) { + this.totalItemsSold = totalItemsSold; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SalesSummary that = (SalesSummary) o; - return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount); + return Objects.equals(totalRevenue, that.totalRevenue) && Objects.equals(totalSales, that.totalSales) && Objects.equals(totalRefunds, that.totalRefunds) && Objects.equals(totalRefundCount, that.totalRefundCount) && Objects.equals(totalItemsSold, that.totalItemsSold); } @Override public int hashCode() { - return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount); + return Objects.hash(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold); } @Override @@ -143,10 +175,69 @@ public class DashboardResponse { ", totalSales=" + totalSales + ", totalRefunds=" + totalRefunds + ", totalRefundCount=" + totalRefundCount + + ", totalItemsSold=" + totalItemsSold + '}'; } } + public static class PaymentMethodData { + private String paymentMethod; + private Long count; + + public PaymentMethodData() { + } + + public PaymentMethodData(String paymentMethod, Long count) { + this.paymentMethod = paymentMethod; + this.count = count; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } +} + + public static class EmployeePerformanceData { + private String employeeName; + private BigDecimal revenue; + + public EmployeePerformanceData() { + } + + public EmployeePerformanceData(String employeeName, BigDecimal revenue) { + this.employeeName = employeeName; + this.revenue = revenue; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } +} + public static class InventorySummary { private Long totalProducts; private Long lowStockProducts; diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java index 32e6a5a7..cef0b620 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -1,9 +1,12 @@ package com.petshop.backend.service; import com.petshop.backend.dto.analytics.DashboardResponse; +import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Inventory; import com.petshop.backend.entity.Product; import com.petshop.backend.entity.Sale; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.InventoryRepository; import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.SaleRepository; @@ -23,28 +26,33 @@ public class AnalyticsService { private final SaleRepository saleRepository; private final InventoryRepository inventoryRepository; private final ProductRepository productRepository; + private final EmployeeRepository employeeRepository; public AnalyticsService(SaleRepository saleRepository, - InventoryRepository inventoryRepository, ProductRepository productRepository) { + InventoryRepository inventoryRepository, ProductRepository productRepository, EmployeeRepository employeeRepository) { this.saleRepository = saleRepository; this.inventoryRepository = inventoryRepository; this.productRepository = productRepository; + this.employeeRepository = employeeRepository; } @Transactional(readOnly = true) - public DashboardResponse getDashboardData(int days, int top) { + public DashboardResponse getDashboardData(int days, int top, User user) { LocalDateTime startDate = LocalDateTime.now().minusDays(days); List sales = saleRepository.findAll().stream() .filter(sale -> sale.getSaleDate().isAfter(startDate)) + .filter(sale -> includeSaleForUser(sale, user)) .collect(Collectors.toList()); DashboardResponse.SalesSummary salesSummary = calculateSalesSummary(sales); - DashboardResponse.InventorySummary inventorySummary = calculateInventorySummary(); + DashboardResponse.InventorySummary inventorySummary = user.getRole() == User.Role.ADMIN ? calculateInventorySummary() : null; List topProducts = calculateTopProducts(sales, top); List dailySales = calculateDailySales(sales, days); + List paymentMethods = calculatePaymentMethods(sales); + List employeePerformance = calculateEmployeePerformance(sales, user); - return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales); + return new DashboardResponse(salesSummary, inventorySummary, topProducts, dailySales, paymentMethods, employeePerformance); } private DashboardResponse.SalesSummary calculateSalesSummary(List sales) { @@ -66,7 +74,13 @@ public class AnalyticsService { .filter(Sale::getIsRefund) .count(); - return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount); + Long totalItemsSold = sales.stream() + .filter(sale -> !sale.getIsRefund()) + .flatMap(sale -> sale.getItems().stream()) + .mapToLong(item -> item.getQuantity()) + .sum(); + + return new DashboardResponse.SalesSummary(totalRevenue, totalSales, totalRefunds, totalRefundCount, totalItemsSold); } private DashboardResponse.InventorySummary calculateInventorySummary() { @@ -138,4 +152,50 @@ public class AnalyticsService { return new ArrayList<>(dailySalesMap.values()); } + + private List calculatePaymentMethods(List sales) { + return sales.stream() + .filter(sale -> !sale.getIsRefund()) + .collect(Collectors.groupingBy( + sale -> sale.getPaymentMethod() == null ? "Unknown" : sale.getPaymentMethod(), + TreeMap::new, + Collectors.counting())) + .entrySet().stream() + .map(entry -> new DashboardResponse.PaymentMethodData(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private List calculateEmployeePerformance(List sales, User user) { + Map employeeRevenue = new TreeMap<>(); + + for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } + String employeeName = sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName(); + employeeRevenue.merge(employeeName, sale.getTotalAmount(), BigDecimal::add); + } + + if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) { + Employee employee = employeeRepository.findByUserId(user.getId()).orElse(null); + if (employee != null) { + String employeeName = employee.getFirstName() + " " + employee.getLastName(); + employeeRevenue.put(employeeName, BigDecimal.ZERO); + } + } + + return employeeRevenue.entrySet().stream() + .map(entry -> new DashboardResponse.EmployeePerformanceData(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private boolean includeSaleForUser(Sale sale, User user) { + if (user.getRole() == User.Role.ADMIN) { + return true; + } + if (user.getRole() == User.Role.STAFF) { + return sale.getEmployee() != null && sale.getEmployee().getUserId() != null && sale.getEmployee().getUserId().equals(user.getId()); + } + return false; + } } From d2a6332633edc208d6329e43c5c6ef72668686d9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 23:34:52 -0600 Subject: [PATCH 021/137] Show staff analytics --- .../api/dto/analytics/DashboardResponse.java | 69 ++++++++++++++++++ .../petshopdesktop/auth/UserSession.java | 4 ++ .../controllers/AnalyticsController.java | 71 ++++++++----------- .../controllers/MainLayoutController.java | 5 +- 4 files changed, 104 insertions(+), 45 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java index 1b29b2b3..38b17b81 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java @@ -8,6 +8,8 @@ public class DashboardResponse { private InventorySummary inventorySummary; private List topProducts; private List dailySales; + private List paymentMethods; + private List employeePerformance; public DashboardResponse() { } @@ -44,11 +46,28 @@ public class DashboardResponse { this.dailySales = dailySales; } + public List getPaymentMethods() { + return paymentMethods; + } + + public void setPaymentMethods(List paymentMethods) { + this.paymentMethods = paymentMethods; + } + + public List getEmployeePerformance() { + return employeePerformance; + } + + public void setEmployeePerformance(List employeePerformance) { + this.employeePerformance = employeePerformance; + } + public static class SalesSummary { private BigDecimal totalRevenue; private Long totalSales; private BigDecimal totalRefunds; private Long totalRefundCount; + private Long totalItemsSold; public SalesSummary() { } @@ -84,6 +103,14 @@ public class DashboardResponse { public void setTotalRefundCount(Long totalRefundCount) { this.totalRefundCount = totalRefundCount; } + + public Long getTotalItemsSold() { + return totalItemsSold; + } + + public void setTotalItemsSold(Long totalItemsSold) { + this.totalItemsSold = totalItemsSold; + } } public static class InventorySummary { @@ -118,4 +145,46 @@ public class DashboardResponse { this.outOfStockProducts = outOfStockProducts; } } + + public static class PaymentMethodData { + private String paymentMethod; + private Long count; + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + } + + public static class EmployeePerformanceData { + private String employeeName; + private BigDecimal revenue; + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java index a578d0e4..cd5646f4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java +++ b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -92,4 +92,8 @@ public class UserSession { public boolean isAdmin() { return Role.ADMIN.equals(role); } + + public boolean isStaff() { + return Role.STAFF.equals(role); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java index 1ab0ffbe..dcae41df 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -9,9 +9,8 @@ import javafx.scene.control.Label; import org.example.petshopdesktop.api.dto.analytics.DailySales; import org.example.petshopdesktop.api.dto.analytics.DashboardResponse; import org.example.petshopdesktop.api.dto.analytics.TopProduct; -import org.example.petshopdesktop.api.dto.sale.SaleResponse; import org.example.petshopdesktop.api.endpoints.AnalyticsApi; -import org.example.petshopdesktop.api.endpoints.SaleApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; import java.math.BigDecimal; @@ -127,16 +126,17 @@ public class AnalyticsController { new Thread(() -> { try { DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10); - List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null); Platform.runLater(() -> { try { + boolean isAdmin = UserSession.getInstance().isAdmin(); loadSummaryData(dashboard); loadSalesOverTime(dashboard); loadTopProductsByRevenue(dashboard); loadTopProductsByQuantity(dashboard); - loadPaymentMethodDistribution(sales); - loadEmployeePerformance(sales); + loadPaymentMethodDistribution(dashboard); + loadEmployeePerformance(dashboard, isAdmin); + applyRoleVisibility(isAdmin); } catch (Exception e) { ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data"); lblError.setText("Error loading analytics data. Please try again."); @@ -157,15 +157,12 @@ public class AnalyticsController { if (dashboard != null) { BigDecimal totalRevenue = BigDecimal.ZERO; Long totalSales = 0L; - Long totalProducts = 0L; - if (dashboard.getSalesSummary() != null) { totalRevenue = dashboard.getSalesSummary().getTotalRevenue() != null ? dashboard.getSalesSummary().getTotalRevenue() : BigDecimal.ZERO; totalSales = dashboard.getSalesSummary().getTotalSales() != null ? dashboard.getSalesSummary().getTotalSales() : 0L; - } - - if (dashboard.getInventorySummary() != null) { - totalProducts = dashboard.getInventorySummary().getTotalProducts() != null ? dashboard.getInventorySummary().getTotalProducts() : 0L; + lblTotalItems.setText(wholeNumber.format(dashboard.getSalesSummary().getTotalItemsSold() != null ? dashboard.getSalesSummary().getTotalItemsSold() : 0L)); + } else { + lblTotalItems.setText(wholeNumber.format(0)); } lblTotalRevenue.setText(currency.format(totalRevenue)); @@ -176,7 +173,6 @@ public class AnalyticsController { avgTransaction = totalRevenue.divide(BigDecimal.valueOf(totalSales), 2, RoundingMode.HALF_UP); } lblAvgTransaction.setText(currency.format(avgTransaction)); - lblTotalItems.setText(wholeNumber.format(totalProducts)); } } @@ -243,24 +239,14 @@ public class AnalyticsController { applyBarChartColor(chartTopQuantity, QUANTITY_COLOR); } - private void loadPaymentMethodDistribution(List sales) throws Exception { - Map paymentMethodCount = sales.stream() - .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) - .collect(Collectors.groupingBy( - sale -> sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "Unknown", - Collectors.counting() - )); - + private void loadPaymentMethodDistribution(DashboardResponse dashboard) throws Exception { chartPaymentMethods.getData().clear(); - List> paymentEntries = paymentMethodCount.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); - - for (Map.Entry entry : paymentEntries) { + List paymentEntries = dashboard.getPaymentMethods() != null ? dashboard.getPaymentMethods() : List.of(); + for (DashboardResponse.PaymentMethodData entry : paymentEntries) { PieChart.Data slice = new PieChart.Data( - entry.getKey() + " (" + entry.getValue() + ")", - entry.getValue() + entry.getPaymentMethod() + " (" + entry.getCount() + ")", + entry.getCount() ); chartPaymentMethods.getData().add(slice); } @@ -269,24 +255,14 @@ public class AnalyticsController { applyPieChartColors(); } - private void loadEmployeePerformance(List sales) throws Exception { - Map employeeRevenue = sales.stream() - .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) - .filter(sale -> sale.getEmployeeName() != null) - .collect(Collectors.groupingBy( - SaleResponse::getEmployeeName, - Collectors.summingDouble(sale -> sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0) - )); - + private void loadEmployeePerformance(DashboardResponse dashboard, boolean isAdmin) throws Exception { XYChart.Series series = new XYChart.Series<>(); - series.setName("Revenue"); + series.setName(isAdmin ? "Revenue" : "My Revenue"); - List> employeeEntries = employeeRevenue.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .toList(); - - for (Map.Entry entry : employeeEntries) { - series.getData().add(new XYChart.Data<>(entry.getKey(), entry.getValue())); + List employeeEntries = dashboard.getEmployeePerformance() != null ? dashboard.getEmployeePerformance() : List.of(); + for (DashboardResponse.EmployeePerformanceData entry : employeeEntries) { + BigDecimal revenue = entry.getRevenue() != null ? entry.getRevenue() : BigDecimal.ZERO; + series.getData().add(new XYChart.Data<>(entry.getEmployeeName(), revenue)); } chartEmployeePerformance.getData().clear(); @@ -294,6 +270,15 @@ public class AnalyticsController { applyBarChartColor(chartEmployeePerformance, EMPLOYEE_COLOR); } + private void applyRoleVisibility(boolean isAdmin) { + chartEmployeePerformance.setVisible(isAdmin); + chartEmployeePerformance.setManaged(isAdmin); + if (chartEmployeePerformance.getParent() != null) { + chartEmployeePerformance.getParent().setVisible(isAdmin); + chartEmployeePerformance.getParent().setManaged(isAdmin); + } + } + private void applyLineChartColor(LineChart chart, String color) { Platform.runLater(() -> { for (XYChart.Series series : chart.getData()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java index d87e181d..4481bc9c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -358,6 +358,7 @@ public class MainLayoutController { lblRole.setText("Leon's Petstore"); boolean isAdmin = session.isAdmin(); + boolean canViewAnalytics = isAdmin || session.isStaff(); btnInventory.setVisible(isAdmin); btnInventory.setManaged(isAdmin); btnSuppliers.setVisible(isAdmin); @@ -384,8 +385,8 @@ public class MainLayoutController { } if (btnAnalytics != null) { - btnAnalytics.setVisible(isAdmin); - btnAnalytics.setManaged(isAdmin); + btnAnalytics.setVisible(canViewAnalytics); + btnAnalytics.setManaged(canViewAnalytics); } btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales"); From 78aac62138c091c6421af211264c711648ff0b0c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 29 Mar 2026 23:50:31 -0600 Subject: [PATCH 022/137] Fix staff analytics --- .../controller/AnalyticsController.java | 8 + .../backend/service/AnalyticsService.java | 6 + .../controllers/AnalyticsController.java | 152 ++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java index 002733fa..9d44f2d4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -5,9 +5,11 @@ import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AnalyticsService; import com.petshop.backend.util.AuthenticationHelper; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/api/v1/analytics") @@ -26,6 +28,12 @@ public class AnalyticsController { public ResponseEntity getDashboard( @RequestParam(defaultValue = "30") int days, @RequestParam(defaultValue = "10") int top) { + if (days < 1 || days > 365) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "days must be between 1 and 365"); + } + if (top < 1 || top > 50) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "top must be between 1 and 50"); + } User user = AuthenticationHelper.getAuthenticatedUser(userRepository); return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user)); } diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java index cef0b620..c14a9511 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -107,6 +107,9 @@ public class AnalyticsService { Map productSalesMap = new HashMap<>(); for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } for (var item : sale.getItems()) { Long productId = item.getProduct().getProdId(); String productName = item.getProduct().getProdName(); @@ -142,6 +145,9 @@ public class AnalyticsService { } for (Sale sale : sales) { + if (sale.getIsRefund()) { + continue; + } LocalDate saleDate = sale.getSaleDate().toLocalDate(); if (dailySalesMap.containsKey(saleDate)) { DashboardResponse.DailySales dailySale = dailySalesMap.get(saleDate); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java index dcae41df..60d14e7f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -9,7 +9,10 @@ import javafx.scene.control.Label; import org.example.petshopdesktop.api.dto.analytics.DailySales; import org.example.petshopdesktop.api.dto.analytics.DashboardResponse; import org.example.petshopdesktop.api.dto.analytics.TopProduct; +import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; +import org.example.petshopdesktop.api.dto.sale.SaleResponse; import org.example.petshopdesktop.api.endpoints.AnalyticsApi; +import org.example.petshopdesktop.api.endpoints.SaleApi; import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; @@ -144,6 +147,31 @@ public class AnalyticsController { } }); } catch (Exception e) { + if (UserSession.getInstance().isStaff()) { + try { + DashboardResponse fallback = buildStaffFallbackDashboard(); + Platform.runLater(() -> { + try { + loadSummaryData(fallback); + loadSalesOverTime(fallback); + loadTopProductsByRevenue(fallback); + loadTopProductsByQuantity(fallback); + loadPaymentMethodDistribution(fallback); + loadEmployeePerformance(fallback, false); + applyRoleVisibility(false); + lblError.setVisible(false); + } catch (Exception inner) { + ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", inner, "Rendering fallback analytics data"); + lblError.setText("Error loading analytics data. Please try again."); + lblError.setVisible(true); + } + }); + return; + } catch (Exception fallbackError) { + ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", fallbackError, "Loading fallback analytics data"); + } + } + Platform.runLater(() -> { ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data"); lblError.setText("Error loading analytics data. Please try again."); @@ -279,6 +307,130 @@ public class AnalyticsController { } } + private DashboardResponse buildStaffFallbackDashboard() throws Exception { + List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null); + String employeeName = UserSession.getInstance().getEmployeeName(); + if (employeeName == null || employeeName.isBlank()) { + employeeName = UserSession.getInstance().getUsername(); + } + final String employeeNameFilter = employeeName; + + List personalSales = sales.stream() + .filter(sale -> sale.getEmployeeName() != null && sale.getEmployeeName().equalsIgnoreCase(employeeNameFilter)) + .toList(); + + DashboardResponse dashboard = new DashboardResponse(); + dashboard.setSalesSummary(buildSalesSummary(personalSales)); + dashboard.setDailySales(buildDailySales(personalSales, 30)); + dashboard.setTopProducts(buildTopProducts(personalSales, 10)); + dashboard.setPaymentMethods(buildPaymentMethods(personalSales)); + dashboard.setEmployeePerformance(List.of(new DashboardResponse.EmployeePerformanceData())); + return dashboard; + } + + private DashboardResponse.SalesSummary buildSalesSummary(List sales) { + DashboardResponse.SalesSummary summary = new DashboardResponse.SalesSummary(); + BigDecimal totalRevenue = BigDecimal.ZERO; + long totalSales = 0L; + BigDecimal totalRefunds = BigDecimal.ZERO; + long totalRefundCount = 0L; + long totalItemsSold = 0L; + + for (SaleResponse sale : sales) { + boolean refund = Boolean.TRUE.equals(sale.getIsRefund()); + BigDecimal amount = sale.getTotalAmount() != null ? sale.getTotalAmount() : BigDecimal.ZERO; + if (refund) { + totalRefunds = totalRefunds.add(amount); + totalRefundCount++; + continue; + } + totalRevenue = totalRevenue.add(amount); + totalSales++; + if (sale.getItems() != null) { + totalItemsSold += sale.getItems().stream().mapToLong(item -> item.getQuantity() == null ? 0 : item.getQuantity()).sum(); + } + } + + summary.setTotalRevenue(totalRevenue); + summary.setTotalSales(totalSales); + summary.setTotalRefunds(totalRefunds); + summary.setTotalRefundCount(totalRefundCount); + summary.setTotalItemsSold(totalItemsSold); + return summary; + } + + private List buildDailySales(List sales, int days) { + Map daily = new LinkedHashMap<>(); + LocalDate start = LocalDate.now().minusDays(days - 1L); + for (int i = 0; i < days; i++) { + LocalDate date = start.plusDays(i); + DailySales row = new DailySales(); + row.setDate(date.toString()); + row.setRevenue(BigDecimal.ZERO); + row.setSalesCount(0L); + daily.put(date, row); + } + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) { + continue; + } + LocalDate date = sale.getSaleDate().toLocalDate(); + DailySales row = daily.get(date); + if (row != null) { + row.setRevenue(row.getRevenue().add(sale.getTotalAmount() == null ? BigDecimal.ZERO : sale.getTotalAmount())); + row.setSalesCount(row.getSalesCount() + 1); + } + } + return new ArrayList<>(daily.values()); + } + + private List buildTopProducts(List sales, int top) { + Map totals = new HashMap<>(); + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getItems() == null) { + continue; + } + for (SaleItemResponse item : sale.getItems()) { + if (item.getProdId() == null) { + continue; + } + TopProduct product = totals.computeIfAbsent(item.getProdId(), id -> { + TopProduct p = new TopProduct(); + p.setProductId(id); + p.setProductName(item.getProductName()); + p.setQuantitySold(0L); + p.setRevenue(BigDecimal.ZERO); + return p; + }); + long quantity = item.getQuantity() == null ? 0 : item.getQuantity(); + BigDecimal unitPrice = item.getUnitPrice() == null ? BigDecimal.ZERO : item.getUnitPrice(); + product.setQuantitySold(product.getQuantitySold() + quantity); + product.setRevenue(product.getRevenue().add(unitPrice.multiply(BigDecimal.valueOf(quantity)))); + } + } + return totals.values().stream() + .sorted((a, b) -> b.getRevenue().compareTo(a.getRevenue())) + .limit(top) + .toList(); + } + + private List buildPaymentMethods(List sales) { + Map totals = new TreeMap<>(); + for (SaleResponse sale : sales) { + if (Boolean.TRUE.equals(sale.getIsRefund())) { + continue; + } + String method = sale.getPaymentMethod() == null || sale.getPaymentMethod().isBlank() ? "Unknown" : sale.getPaymentMethod(); + totals.merge(method, 1L, Long::sum); + } + return totals.entrySet().stream().map(entry -> { + DashboardResponse.PaymentMethodData data = new DashboardResponse.PaymentMethodData(); + data.setPaymentMethod(entry.getKey()); + data.setCount(entry.getValue()); + return data; + }).toList(); + } + private void applyLineChartColor(LineChart chart, String color) { Platform.runLater(() -> { for (XYChart.Series series : chart.getData()) { From 410b68520af9bfc6d3f0d4d02074ca26fcb8855c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 00:03:27 -0600 Subject: [PATCH 023/137] Fix android backend url --- android/app/build.gradle.kts | 25 +++++++++++++++++- .../petstoremobile/api/RetrofitClient.java | 26 ++++++++++++------- android/local.properties.template | 7 +++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 552480f0..e3955e2f 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,7 +1,23 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) } +val localProperties = Properties().apply { + val file = rootProject.file("local.properties") + if (file.exists()) { + file.inputStream().use { load(it) } + } +} + +fun quoted(value: String): String = "\"$value\"" + +val emulatorBackendUrl = + (localProperties.getProperty("petstore.backend.emulatorUrl") ?: "http://10.0.2.2:8080/").trim() +val deviceBackendUrl = + (localProperties.getProperty("petstore.backend.deviceUrl") ?: "http://10.0.0.200:8080/").trim() + android { namespace = "com.example.petstoremobile" compileSdk = 36 @@ -14,6 +30,13 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "EMULATOR_BACKEND_URL", quoted(emulatorBackendUrl)) + buildConfigField("String", "DEVICE_BACKEND_URL", quoted(deviceBackendUrl)) + } + + buildFeatures { + buildConfig = true } buildTypes { @@ -62,4 +85,4 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index 3a93deb2..b08af532 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -2,7 +2,9 @@ package com.example.petstoremobile.api; import android.content.Context; import android.os.Build; +import android.util.Log; +import com.example.petstoremobile.BuildConfig; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.AuthInterceptor; @@ -15,22 +17,28 @@ import java.util.concurrent.TimeUnit; //Retrofit client Used for API calls public class RetrofitClient { + private static final String TAG = "RetrofitClient"; public static final String BASE_URL = getBaseUrl(); // Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device private static String getBaseUrl() { - if (Build.FINGERPRINT.contains("generic") - || Build.FINGERPRINT.contains("unknown") + String url = isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL; + Log.i(TAG, "Using backend URL: " + url + " (emulator=" + isEmulator() + ")"); + return url; + } + + private static boolean isEmulator() { + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") - || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || "google_sdk".equals(Build.PRODUCT)) { - return "http://10.0.2.2:8080/"; //emulator testing - } else { - return "http://10.0.0.200:8080/"; //Hardware testing - } + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_gphone") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")); } private static Retrofit retrofit = null; @@ -123,4 +131,4 @@ public class RetrofitClient { return getClient(context).create(CategoryApi.class); } -} \ No newline at end of file +} diff --git a/android/local.properties.template b/android/local.properties.template index 788baf64..e41a73bc 100644 --- a/android/local.properties.template +++ b/android/local.properties.template @@ -9,3 +9,10 @@ # Copy this file to local.properties and update the path below: sdk.dir=/path/to/your/android/sdk + +# Optional backend overrides for Android development: +# Emulator default should usually stay 10.0.2.2 for the local machine. +petstore.backend.emulatorUrl=http\://10.0.2.2\:8080/ + +# Physical device example. Replace with the machine IP running the backend. +petstore.backend.deviceUrl=http\://10.0.0.200\:8080/ From 00c5198c477e5ae55cecb2664e8433bee8356a24 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Mon, 30 Mar 2026 05:38:15 -0600 Subject: [PATCH 024/137] Appointments, account stuff, adopt a pet changes --- .gitignore | 1 + .../backend/controller/AuthController.java | 15 +- .../controller/CustomerPetController.java | 118 +++ .../dto/appointment/AppointmentRequest.java | 18 +- .../dto/appointment/AppointmentResponse.java | 20 + .../backend/dto/auth/UserInfoResponse.java | 17 +- .../dto/customerpet/CustomerPetRequest.java | 66 ++ .../dto/customerpet/CustomerPetResponse.java | 123 ++++ .../petshop/backend/entity/Appointment.java | 17 + .../petshop/backend/entity/CustomerPet.java | 137 ++++ .../repository/CustomerPetRepository.java | 16 + .../backend/service/AppointmentService.java | 84 ++- .../backend/service/CustomerPetService.java | 163 ++++ .../db/migration/V10__adoption_service.sql | 2 + .../V11__appointment_customer_pet.sql | 7 + .../db/migration/V9__customer_pet.sql | 11 + web/app/adopt/[id]/page.js | 2 + web/app/adopt/page.js | 1 + web/app/appointments/page.js | 640 ++++++++++++++++ web/app/globals.css | 693 +++++++++++++++++- web/app/products/[id]/page.js | 57 ++ web/app/products/page.js | 133 ++++ web/app/profile/page.js | 248 ++++++- web/components/Navigation.js | 2 +- web/components/PetCard.js | 8 +- web/components/PetProfile.js | 8 +- web/components/ProductCard.js | 18 + web/components/ProductProfile.js | 34 + web/public/images/home/navimages/about.jpg | Bin 0 -> 251455 bytes web/public/images/pet-placeholder.png | Bin 0 -> 105870 bytes 30 files changed, 2611 insertions(+), 48 deletions(-) create mode 100644 .gitignore create mode 100644 backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java create mode 100644 backend/src/main/java/com/petshop/backend/entity/CustomerPet.java create mode 100644 backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java create mode 100644 backend/src/main/java/com/petshop/backend/service/CustomerPetService.java create mode 100644 backend/src/main/resources/db/migration/V10__adoption_service.sql create mode 100644 backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql create mode 100644 backend/src/main/resources/db/migration/V9__customer_pet.sql create mode 100644 web/app/appointments/page.js create mode 100644 web/app/products/[id]/page.js create mode 100644 web/app/products/page.js create mode 100644 web/components/ProductCard.js create mode 100644 web/components/ProductProfile.js create mode 100644 web/public/images/home/navimages/about.jpg create mode 100644 web/public/images/pet-placeholder.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c4c4ffc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.zip diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index b426aadc..106ea66f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -9,6 +9,7 @@ import com.petshop.backend.dto.auth.RegisterResponse; import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.EmployeeStoreRepository; import com.petshop.backend.repository.UserRepository; @@ -47,8 +48,9 @@ public class AuthController { private final EmployeeRepository employeeRepository; private final EmployeeStoreRepository employeeStoreRepository; private final AvatarStorageService avatarStorageService; + private final CustomerRepository customerRepository; - public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService) { + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService, CustomerRepository customerRepository) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.jwtUtil = jwtUtil; @@ -57,6 +59,7 @@ public class AuthController { this.employeeRepository = employeeRepository; this.employeeStoreRepository = employeeStoreRepository; this.avatarStorageService = avatarStorageService; + this.customerRepository = customerRepository; } @PostMapping("/register") @@ -147,6 +150,7 @@ public class AuthController { User user = getAuthenticatedUser(); EmployeeStore employeeStore = resolveEmployeeStore(user); + Long customerId = resolveCustomerId(user); return ResponseEntity.ok(new UserInfoResponse( user.getId(), @@ -156,6 +160,7 @@ public class AuthController { user.getPhone(), avatarStorageService.toOwnerAvatarUrl(user), user.getRole().name(), + customerId, employeeStore != null ? employeeStore.getStore().getStoreId() : null, employeeStore != null ? employeeStore.getStore().getStoreName() : null )); @@ -216,6 +221,7 @@ public class AuthController { userBusinessLinkageService.syncLinkedRecords(updatedUser); EmployeeStore employeeStore = resolveEmployeeStore(updatedUser); + Long customerId = resolveCustomerId(updatedUser); return ResponseEntity.ok(new UserInfoResponse( updatedUser.getId(), @@ -225,6 +231,7 @@ public class AuthController { updatedUser.getPhone(), avatarStorageService.toOwnerAvatarUrl(updatedUser), updatedUser.getRole().name(), + customerId, employeeStore != null ? employeeStore.getStore().getStoreId() : null, employeeStore != null ? employeeStore.getStore().getStoreName() : null )); @@ -240,6 +247,12 @@ public class AuthController { .orElse(null); } + private Long resolveCustomerId(User user) { + return customerRepository.findByUserId(user.getId()) + .map(c -> c.getCustomerId()) + .orElse(null); + } + private String trimToNull(String value) { if (value == null) { return null; diff --git a/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java b/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java new file mode 100644 index 00000000..4fd6648b --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java @@ -0,0 +1,118 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.customerpet.CustomerPetRequest; +import com.petshop.backend.dto.customerpet.CustomerPetResponse; +import com.petshop.backend.service.CatalogImageStorageService; +import com.petshop.backend.service.CustomerPetService; +import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.repository.CustomerPetRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/my-pets") +@PreAuthorize("hasRole('CUSTOMER')") +public class CustomerPetController { + + private final CustomerPetService customerPetService; + private final CustomerPetRepository customerPetRepository; + private final CustomerRepository customerRepository; + private final UserRepository userRepository; + private final CatalogImageStorageService catalogImageStorageService; + + public CustomerPetController(CustomerPetService customerPetService, + CustomerPetRepository customerPetRepository, + CustomerRepository customerRepository, + UserRepository userRepository, + CatalogImageStorageService catalogImageStorageService) { + this.customerPetService = customerPetService; + this.customerPetRepository = customerPetRepository; + this.customerRepository = customerRepository; + this.userRepository = userRepository; + this.catalogImageStorageService = catalogImageStorageService; + } + + @GetMapping + public ResponseEntity> getMyPets() { + + return ResponseEntity.ok(customerPetService.getMyPets()); + } + + @PostMapping + public ResponseEntity createPet(@Valid @RequestBody CustomerPetRequest request) { + + return ResponseEntity.status(HttpStatus.CREATED).body(customerPetService.createPet(request)); + } + + @PutMapping("/{id}") + public ResponseEntity updatePet(@PathVariable Long id, @Valid @RequestBody CustomerPetRequest request) { + + return ResponseEntity.ok(customerPetService.updatePet(id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePet(@PathVariable Long id) { + customerPetService.deletePet(id); + + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{id}/image") + public ResponseEntity uploadImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { + try { + + return ResponseEntity.ok(customerPetService.uploadImage(id, image)); + } + + catch (IllegalArgumentException ex) { + Map error = new HashMap<>(); + error.put("message", ex.getMessage()); + + return ResponseEntity.badRequest().body(error); + } + + catch (IOException ex) { + Map error = new HashMap<>(); + error.put("message", "Failed to upload image: " + ex.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + @GetMapping("/{id}/image") + public ResponseEntity getImage(@PathVariable Long id) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElse(null); + + if (pet == null || pet.getImageUrl() == null || pet.getImageUrl().isBlank()) { + + return ResponseEntity.notFound().build(); + } + + Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl()); + MediaType mediaType = catalogImageStorageService.resolveMediaType(resource); + + return ResponseEntity.ok().contentType(mediaType).body(resource); + } + + @DeleteMapping("/{id}/image") + public ResponseEntity deleteImage(@PathVariable Long id) { + + return ResponseEntity.ok(customerPetService.deleteImage(id)); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 247e5ae4..8423d090 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -1,6 +1,5 @@ package com.petshop.backend.dto.appointment; -import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalTime; @@ -26,9 +25,10 @@ public class AppointmentRequest { @NotNull(message = "Appointment status is required") private String appointmentStatus; - @NotEmpty(message = "At least one pet must be specified") private List petIds; + private List customerPetIds; + public Long getCustomerId() { return customerId; } @@ -85,6 +85,14 @@ public class AppointmentRequest { this.petIds = petIds; } + public List getCustomerPetIds() { + return customerPetIds; + } + + public void setCustomerPetIds(List customerPetIds) { + this.customerPetIds = customerPetIds; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -96,12 +104,13 @@ public class AppointmentRequest { Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && - Objects.equals(petIds, that.petIds); + Objects.equals(petIds, that.petIds) && + Objects.equals(customerPetIds, that.customerPetIds); } @Override public int hashCode() { - return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds); + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds); } @Override @@ -114,6 +123,7 @@ public class AppointmentRequest { ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + ", petIds=" + petIds + + ", customerPetIds=" + customerPetIds + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java index c7d2e8d7..f6398248 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -19,6 +19,8 @@ public class AppointmentResponse { private String appointmentStatus; private List petNames; private List petIds; + private List customerPetNames; + private List customerPetIds; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -138,6 +140,24 @@ public class AppointmentResponse { this.petIds = petIds; } + public List getCustomerPetNames() { + + return customerPetNames; + } + + public void setCustomerPetNames(List customerPetNames) { + this.customerPetNames = customerPetNames; + } + + public List getCustomerPetIds() { + + return customerPetIds; + } + + public void setCustomerPetIds(List customerPetIds) { + this.customerPetIds = customerPetIds; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java index ba714a49..84372638 100644 --- a/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/auth/UserInfoResponse.java @@ -10,13 +10,14 @@ public class UserInfoResponse { private String phone; private String avatarUrl; private String role; + private Long customerId; private Long storeId; private String storeName; public UserInfoResponse() { } - public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long storeId, String storeName) { + public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) { this.id = id; this.username = username; this.email = email; @@ -24,6 +25,7 @@ public class UserInfoResponse { this.phone = phone; this.avatarUrl = avatarUrl; this.role = role; + this.customerId = customerId; this.storeId = storeId; this.storeName = storeName; } @@ -84,6 +86,15 @@ public class UserInfoResponse { this.role = role; } + public Long getCustomerId() { + + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + public Long getStoreId() { return storeId; } @@ -112,13 +123,14 @@ public class UserInfoResponse { Objects.equals(phone, that.phone) && Objects.equals(avatarUrl, that.avatarUrl) && Objects.equals(role, that.role) && + Objects.equals(customerId, that.customerId) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName); } @Override public int hashCode() { - return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, storeId, storeName); + return Objects.hash(id, username, email, fullName, phone, avatarUrl, role, customerId, storeId, storeName); } @Override @@ -131,6 +143,7 @@ public class UserInfoResponse { ", phone='" + phone + '\'' + ", avatarUrl='" + avatarUrl + '\'' + ", role='" + role + '\'' + + ", customerId=" + customerId + ", storeId=" + storeId + ", storeName='" + storeName + '\'' + '}'; diff --git a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java new file mode 100644 index 00000000..b4b37355 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java @@ -0,0 +1,66 @@ +package com.petshop.backend.dto.customerpet; + +import jakarta.validation.constraints.NotBlank; + +import java.util.Objects; + +public class CustomerPetRequest { + + @NotBlank(message = "Pet name is required") + private String petName; + + @NotBlank(message = "Species is required") + private String species; + + private String breed; + + public String getPetName() { + + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + + return true; + } + + if (o == null || getClass() != o.getClass()) { + + return false; + } + + CustomerPetRequest that = (CustomerPetRequest) o; + + return Objects.equals(petName, that.petName) && Objects.equals(species, that.species) && Objects.equals(breed, that.breed); + } + + @Override + public int hashCode() { + + return Objects.hash(petName, species, breed); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java new file mode 100644 index 00000000..8e9ab37d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java @@ -0,0 +1,123 @@ +package com.petshop.backend.dto.customerpet; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class CustomerPetResponse { + + private Long customerPetId; + private Long customerId; + private String petName; + private String species; + private String breed; + private String imageUrl; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public CustomerPetResponse() { + } + + public CustomerPetResponse(Long customerPetId, Long customerId, String petName, String species, String breed, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.customerPetId = customerPetId; + this.customerId = customerId; + this.petName = petName; + this.species = species; + this.breed = breed; + this.imageUrl = imageUrl; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getCustomerPetId() { + + return customerPetId; + } + + public void setCustomerPetId(Long customerPetId) { + this.customerPetId = customerPetId; + } + + public Long getCustomerId() { + + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getPetName() { + + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getImageUrl() { + + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDateTime getCreatedAt() { + + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + CustomerPetResponse that = (CustomerPetResponse) o; + + return Objects.equals(customerPetId, that.customerPetId); + } + + @Override + public int hashCode() { + + return Objects.hash(customerPetId); + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java index 101ff885..cec10224 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Appointment.java +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -48,6 +48,14 @@ public class Appointment { ) private Set pets = new HashSet<>(); + @ManyToMany + @JoinTable( + name = "appointment_customer_pet", + joinColumns = @JoinColumn(name = "appointment_id"), + inverseJoinColumns = @JoinColumn(name = "customer_pet_id") + ) + private Set customerPets = new HashSet<>(); + @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -136,6 +144,15 @@ public class Appointment { this.pets = pets; } + public Set getCustomerPets() { + + return customerPets; + } + + public void setCustomerPets(Set customerPets) { + this.customerPets = customerPets; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java b/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java new file mode 100644 index 00000000..df75df8c --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java @@ -0,0 +1,137 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "customer_pet") +public class CustomerPet { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "customer_pet_id") + private Long customerPetId; + + @ManyToOne + @JoinColumn(name = "customer_id", nullable = false) + private Customer customer; + + @Column(name = "pet_name", nullable = false, length = 50) + private String petName; + + @Column(nullable = false, length = 50) + private String species; + + @Column(length = 50) + private String breed; + + @Column(name = "image_url", length = 255) + private String imageUrl; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public CustomerPet() { + } + + public Long getCustomerPetId() { + + return customerPetId; + } + + public void setCustomerPetId(Long customerPetId) { + this.customerPetId = customerPetId; + } + + public Customer getCustomer() { + + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public String getPetName() { + + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getImageUrl() { + + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDateTime getCreatedAt() { + + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + CustomerPet that = (CustomerPet) o; + return Objects.equals(customerPetId, that.customerPetId); + } + + @Override + public int hashCode() { + + return Objects.hash(customerPetId); + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java new file mode 100644 index 00000000..8d08f8b9 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java @@ -0,0 +1,16 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.CustomerPet; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CustomerPetRepository extends JpaRepository { + + List findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId); + + Optional findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId); +} diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 9abcf831..3fa68542 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -5,6 +5,7 @@ import com.petshop.backend.dto.appointment.AppointmentResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.CustomerPet; import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.Pet; @@ -12,6 +13,7 @@ import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.CustomerPetRepository; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.EmployeeStoreRepository; @@ -40,6 +42,7 @@ public class AppointmentService { private final AppointmentRepository appointmentRepository; private final CustomerRepository customerRepository; + private final CustomerPetRepository customerPetRepository; private final ServiceRepository serviceRepository; private final PetRepository petRepository; private final StoreRepository storeRepository; @@ -47,9 +50,10 @@ public class AppointmentService { private final EmployeeRepository employeeRepository; private final EmployeeStoreRepository employeeStoreRepository; - public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) { + public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, CustomerPetRepository customerPetRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) { this.appointmentRepository = appointmentRepository; this.customerRepository = customerRepository; + this.customerPetRepository = customerPetRepository; this.serviceRepository = serviceRepository; this.petRepository = petRepository; this.storeRepository = storeRepository; @@ -107,7 +111,16 @@ public class AppointmentService { validateStoreAccess(store.getStoreId()); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); - Set pets = fetchPets(request.getPetIds()); + boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); + boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); + + if (!hasPetIds && !hasCustomerPetIds) { + + throw new IllegalArgumentException("Please specify at least one pet."); + } + + Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -117,6 +130,7 @@ public class AppointmentService { appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); + appointment.setCustomerPets(customerPets); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -141,7 +155,16 @@ public class AppointmentService { validateStoreAccess(store.getStoreId()); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); - Set pets = fetchPets(request.getPetIds()); + boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); + boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); + + if (!hasPetIds && !hasCustomerPetIds) { + + throw new IllegalArgumentException("Please specify at least one pet."); + } + + Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); appointment.setCustomer(customer); appointment.setStore(store); @@ -150,6 +173,7 @@ public class AppointmentService { appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); + appointment.setCustomerPets(customerPets); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -213,6 +237,17 @@ public class AppointmentService { return pets; } + private Set fetchCustomerPets(List customerPetIds) { + Set customerPets = new HashSet<>(); + for (Long customerPetId : customerPetIds) { + CustomerPet customerPet = customerPetRepository.findById(customerPetId) + .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); + customerPets.add(customerPet); + } + + return customerPets; + } + private AppointmentResponse mapToResponse(Appointment appointment) { List petNames = appointment.getPets().stream() .map(Pet::getPetName) @@ -222,22 +257,33 @@ public class AppointmentService { .map(Pet::getPetId) .collect(Collectors.toList()); - return new AppointmentResponse( - appointment.getAppointmentId(), - appointment.getCustomer().getCustomerId(), - appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName(), - appointment.getStore().getStoreId(), - appointment.getStore().getStoreName(), - appointment.getService().getServiceId(), - appointment.getService().getServiceName(), - appointment.getAppointmentDate(), - appointment.getAppointmentTime(), - appointment.getAppointmentStatus(), - petNames, - petIds, - appointment.getCreatedAt(), - appointment.getUpdatedAt() - ); + List customerPetNames = appointment.getCustomerPets().stream() + .map(CustomerPet::getPetName) + .collect(Collectors.toList()); + + List customerPetIds = appointment.getCustomerPets().stream() + .map(CustomerPet::getCustomerPetId) + .collect(Collectors.toList()); + + AppointmentResponse response = new AppointmentResponse(); + response.setAppointmentId(appointment.getAppointmentId()); + response.setCustomerId(appointment.getCustomer().getCustomerId()); + response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName()); + response.setStoreId(appointment.getStore().getStoreId()); + response.setStoreName(appointment.getStore().getStoreName()); + response.setServiceId(appointment.getService().getServiceId()); + response.setServiceName(appointment.getService().getServiceName()); + response.setAppointmentDate(appointment.getAppointmentDate()); + response.setAppointmentTime(appointment.getAppointmentTime()); + response.setAppointmentStatus(appointment.getAppointmentStatus()); + response.setPetNames(petNames); + response.setPetIds(petIds); + response.setCustomerPetNames(customerPetNames); + response.setCustomerPetIds(customerPetIds); + response.setCreatedAt(appointment.getCreatedAt()); + response.setUpdatedAt(appointment.getUpdatedAt()); + + return response; } private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java b/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java new file mode 100644 index 00000000..fa424737 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java @@ -0,0 +1,163 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.customerpet.CustomerPetRequest; +import com.petshop.backend.dto.customerpet.CustomerPetResponse; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.CustomerPetRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.util.AuthenticationHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +@Service +public class CustomerPetService { + + private final CustomerPetRepository customerPetRepository; + private final CustomerRepository customerRepository; + private final UserRepository userRepository; + private final CatalogImageStorageService catalogImageStorageService; + + public CustomerPetService(CustomerPetRepository customerPetRepository, + CustomerRepository customerRepository, + UserRepository userRepository, + CatalogImageStorageService catalogImageStorageService) { + this.customerPetRepository = customerPetRepository; + this.customerRepository = customerRepository; + this.userRepository = userRepository; + this.catalogImageStorageService = catalogImageStorageService; + } + + @Transactional(readOnly = true) + public List getMyPets() { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + + return customerPetRepository.findByCustomerCustomerIdOrderByCreatedAtDesc(customer.getCustomerId()) + .stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + } + + @Transactional + public CustomerPetResponse createPet(CustomerPetRequest request) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + + CustomerPet pet = new CustomerPet(); + pet.setCustomer(customer); + pet.setPetName(request.getPetName()); + pet.setSpecies(request.getSpecies()); + pet.setBreed(request.getBreed()); + + pet = customerPetRepository.save(pet); + + return mapToResponse(pet); + } + + @Transactional + public CustomerPetResponse updatePet(Long id, CustomerPetRequest request) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + + pet.setPetName(request.getPetName()); + pet.setSpecies(request.getSpecies()); + pet.setBreed(request.getBreed()); + + pet = customerPetRepository.save(pet); + + return mapToResponse(pet); + } + + @Transactional + public void deletePet(Long id) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + deleteStoredImageIfPresent(pet.getImageUrl()); + + customerPetRepository.delete(pet); + } + + @Transactional + public CustomerPetResponse uploadImage(Long id, MultipartFile file) throws IOException { + validateImageFile(file); + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + deleteStoredImageIfPresent(pet.getImageUrl()); + pet.setImageUrl(catalogImageStorageService.storePetImage(file)); + + return mapToResponse(customerPetRepository.save(pet)); + } + + @Transactional + public CustomerPetResponse deleteImage(Long id) { + Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); + CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + deleteStoredImageIfPresent(pet.getImageUrl()); + pet.setImageUrl(null); + + return mapToResponse(customerPetRepository.save(pet)); + } + + private CustomerPetResponse mapToResponse(CustomerPet pet) { + return new CustomerPetResponse( + pet.getCustomerPetId(), + pet.getCustomer().getCustomerId(), + pet.getPetName(), + pet.getSpecies(), + pet.getBreed(), + pet.getImageUrl() != null && !pet.getImageUrl().isBlank() + ? "/api/v1/my-pets/" + pet.getCustomerPetId() + "/image" + : null, + pet.getCreatedAt(), + pet.getUpdatedAt() + ); + } + + private void validateImageFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + + throw new IllegalArgumentException("Please select an image to upload"); + } + + if (file.getSize() > 5 * 1024 * 1024) { + + throw new IllegalArgumentException("Image file size must be less than 5MB"); + } + + String contentType = file.getContentType(); + + if (contentType == null) { + + throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + } + + String normalized = contentType.toLowerCase(Locale.ROOT); + + if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { + + throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + } + } + + private void deleteStoredImageIfPresent(String storedImagePath) { + if (storedImagePath == null || storedImagePath.isBlank()) { + + return; + } + + try { + catalogImageStorageService.deletePetImage(storedImagePath); + } + + catch (IOException ignored) { + } + } +} diff --git a/backend/src/main/resources/db/migration/V10__adoption_service.sql b/backend/src/main/resources/db/migration/V10__adoption_service.sql new file mode 100644 index 00000000..dd5a57ca --- /dev/null +++ b/backend/src/main/resources/db/migration/V10__adoption_service.sql @@ -0,0 +1,2 @@ +INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice) +VALUES ('Pet Adoption', 'Schedule a visit to meet and adopt an available pet', 30, 0.00); diff --git a/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql b/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql new file mode 100644 index 00000000..d112fda0 --- /dev/null +++ b/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS appointment_customer_pet ( + appointment_id BIGINT NOT NULL, + customer_pet_id BIGINT NOT NULL, + PRIMARY KEY (appointment_id, customer_pet_id), + FOREIGN KEY (appointment_id) REFERENCES appointment(appointmentId), + FOREIGN KEY (customer_pet_id) REFERENCES customer_pet(customer_pet_id) +); diff --git a/backend/src/main/resources/db/migration/V9__customer_pet.sql b/backend/src/main/resources/db/migration/V9__customer_pet.sql new file mode 100644 index 00000000..0981bca2 --- /dev/null +++ b/backend/src/main/resources/db/migration/V9__customer_pet.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS customer_pet ( + customer_pet_id BIGINT AUTO_INCREMENT PRIMARY KEY, + customer_id BIGINT NOT NULL, + pet_name VARCHAR(50) NOT NULL, + species VARCHAR(50) NOT NULL, + breed VARCHAR(50) NULL, + image_url VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customer(customerId) +); diff --git a/web/app/adopt/[id]/page.js b/web/app/adopt/[id]/page.js index 21a24aa4..0834ca01 100644 --- a/web/app/adopt/[id]/page.js +++ b/web/app/adopt/[id]/page.js @@ -38,12 +38,14 @@ export default function PetDetailPage() { {!loading && !error && pet && ( )}
diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 9a30f6ea..430a1653 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -119,6 +119,7 @@ export default function AdoptPage() { petName={pet.petName} petSpecies={pet.petSpecies} petStatus={pet.petStatus} + imageUrl={pet.imageUrl} /> ))}
diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js new file mode 100644 index 00000000..d3f7943e --- /dev/null +++ b/web/app/appointments/page.js @@ -0,0 +1,640 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useAuth } from "@/context/AuthContext"; + +const API_BASE = ""; + +const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", +]; + +function DatePicker({ value, minDate, onChange }) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const min = minDate ? new Date(minDate + "T00:00:00") : today; + + const parsed = value ? new Date(value + "T00:00:00") : null; + const [viewYear, setViewYear] = useState(parsed ? parsed.getFullYear() : min.getFullYear()); + const [viewMonth, setViewMonth] = useState(parsed ? parsed.getMonth() : min.getMonth()); + + function prevMonth() { + if (viewMonth === 0) { + setViewMonth(11); + setViewYear((y) => y - 1); + } + + else { + setViewMonth((m) => m - 1); + } + } + + function nextMonth() { + if (viewMonth === 11) { + setViewMonth(0); setViewYear((y) => y + 1); + } + + else { + setViewMonth((m) => m + 1); + } + } + + const firstDay = new Date(viewYear, viewMonth, 1).getDay(); + const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); + + const minYear = min.getFullYear(); + const minMonth = min.getMonth(); + const isPrevDisabled = viewYear < minYear || (viewYear === minYear && viewMonth <= minMonth); + + function selectDay(day) { + const d = new Date(viewYear, viewMonth, day); + if (d < min) { + return; + } + const iso = `${viewYear}-${String(viewMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + + onChange(iso); + } + + function isSelected(day) { + if (!parsed) return false; + return parsed.getFullYear() === viewYear && parsed.getMonth() === viewMonth && parsed.getDate() === day; + } + + function isDisabled(day) { + return new Date(viewYear, viewMonth, day) < min; + } + + const cells = []; + for (let i = 0; i < firstDay; i++) cells.push(null); + for (let d = 1; d <= daysInMonth; d++) cells.push(d); + + const s = { + widget: { + border: "1px solid #ddd", + borderRadius: "10px", + overflow: "hidden", + background: "white", + userSelect: "none", + fontFamily: "inherit", + }, + header: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + background: "orange", + padding: "0.55rem 0.75rem", + }, + monthLabel: { + fontSize: "0.95rem", + fontWeight: 700, + color: "white", + }, + nav: { + background: "none", + border: "none", + color: "white", + fontSize: "1.5rem", + lineHeight: 1, + cursor: "pointer", + padding: "0 0.4rem", + borderRadius: "4px", + }, + grid: { + display: "grid", + gridTemplateColumns: "repeat(7, 1fr)", + gap: "3px", + padding: "0.6rem", + }, + dayName: { + textAlign: "center", + fontSize: "0.7rem", + fontWeight: 700, + color: "#aaa", + padding: "0.25rem 0", + textTransform: "uppercase", + }, + dayBase: { + display: "flex", + alignItems: "center", + justifyContent: "center", + aspectRatio: "1 / 1", + border: "none", + borderRadius: "6px", + background: "none", + fontSize: "0.875rem", + cursor: "pointer", + color: "#333", + fontFamily: "inherit", + padding: 0, + width: "100%", + }, + daySelected: { + background: "orange", + color: "white", + fontWeight: 700, + }, + dayDisabled: { + color: "#ccc", + cursor: "default", + }, + selectedLabel: { + textAlign: "center", + fontSize: "0.82rem", + color: "#666", + padding: "0.35rem 0.5rem 0.5rem", + borderTop: "1px solid #f0f0f0", + }, + }; + + return ( +
+
+ + {MONTHS[viewMonth]} {viewYear} + +
+
+ {DAYS.map((d) => ( + {d} + ))} + {cells.map((day, i) => + day === null ? ( + + ) : ( + + ) + )} +
+ {parsed && ( +
+ Selected: {MONTHS[parsed.getMonth()]} {parsed.getDate()}, {parsed.getFullYear()} +
+ )} +
+ ); +} + +export default function AppointmentsPage() { + const { user, token, loading: authLoading } = useAuth(); + const router = useRouter(); + const searchParams = useSearchParams(); + const preselectedPetId = searchParams.get("petId"); + const didPreselectRef = useRef(false); + + const [stores, setStores] = useState([]); + const [services, setServices] = useState([]); + const [allPets, setAllPets] = useState([]); + const [customerPets, setCustomerPets] = useState([]); + const [availableSlots, setAvailableSlots] = useState([]); + + const [storeId, setStoreId] = useState(""); + const [serviceId, setServiceId] = useState(""); + const [appointmentDate, setAppointmentDate] = useState(""); + const [appointmentTime, setAppointmentTime] = useState(""); + const [selectedPetIds, setSelectedPetIds] = useState([]); + + const [loadingSlots, setLoadingSlots] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const [appointments, setAppointments] = useState([]); + const [loadingAppointments, setLoadingAppointments] = useState(false); + + useEffect(() => { + if (!authLoading && !user) { + router.push("/login"); + } + + }, [authLoading, user, router]); + + useEffect(() => { + if (!token) { + return; + } + + fetch(`${API_BASE}/api/v1/dropdowns/stores`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((r) => r.json()) + .then(setStores) + .catch(() => {}); + + fetch(`${API_BASE}/api/v1/services?size=100`) + .then((r) => r.json()) + .then((data) => setServices(data.content ?? [])) + .catch(() => {}); + + fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,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]); + + useEffect(() => { + if (didPreselectRef.current) { + return; + } + if (!preselectedPetId || services.length === 0 || allPets.length === 0) { + return; + } + + const adoptionSvc = services.find((s) => + s.serviceName.toLowerCase().includes("adopt") + ); + + if (adoptionSvc) { + setServiceId(String(adoptionSvc.serviceId)); + } + setSelectedPetIds([Number(preselectedPetId)]); + didPreselectRef.current = true; + }, [preselectedPetId, services, allPets]); + + const loadAppointments = useCallback(() => { + + if (!token) { + return; + } + setLoadingAppointments(true); + fetch(`${API_BASE}/api/v1/appointments?size=50&sort=appointmentDate,desc`, { + headers: {Authorization: `Bearer ${token}`}, + }) + .then((r) => r.json()) + .then((data) => setAppointments(data.content ?? [])) + .catch(() => {}) + .finally(() => setLoadingAppointments(false)); + }, [token]); + + useEffect(() => { + loadAppointments(); + }, [loadAppointments]); + + useEffect(() => { + if (!storeId || !serviceId || !appointmentDate) { + setAvailableSlots([]); + setAppointmentTime(""); + + return; + } + setLoadingSlots(true); + setAppointmentTime(""); + const params = new URLSearchParams({ storeId, serviceId, date: appointmentDate }); + fetch(`${API_BASE}/api/v1/appointments/availability?${params}`) + .then((r) => { + if (!r.ok) { + throw new Error("Failed to check availability"); + } + + return r.json(); + }) + .then(setAvailableSlots) + .catch(() => setAvailableSlots([])) + .finally(() => setLoadingSlots(false)); + }, [storeId, serviceId, appointmentDate]); + + const selectedService = services.find((s) => s.serviceId === Number(serviceId)); + const isAdoptionService = selectedService ? selectedService.serviceName.toLowerCase().includes("adopt") : false; + const isCustomerPetService = !!selectedService && !isAdoptionService; + + const adoptablePets = allPets.filter( + (p) => p.petStatus && p.petStatus.toLowerCase() === "available" + ); + + function handleServiceChange(newServiceId) { + setServiceId(newServiceId); + setSelectedPetIds([]); + } + + function togglePet(petId) { + if (isAdoptionService) { + setSelectedPetIds((prev) => + prev.includes(petId) ? [] : [petId] + ); + } + + else { + setSelectedPetIds((prev) => + prev.includes(petId) ? prev.filter((id) => id !== petId) : [...prev, petId] + ); + } + } + + function formatTime(timeStr) { + const [h, m] = timeStr.split(":"); + const hour = parseInt(h, 10); + const ampm = hour >= 12 ? "PM" : "AM"; + const display = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + + return `${display}:${m} ${ampm}`; + } + + function getMinDate() { + const d = new Date(); + d.setDate(d.getDate() + 1); + + return d.toISOString().split("T")[0]; + } + + const formValid = + storeId && serviceId && appointmentDate && appointmentTime && selectedPetIds.length > 0; + + async function handleSubmit(e) { + e.preventDefault(); + setError(null); + setSuccess(null); + + if (!user?.customerId) { + setError("Customer account not found. Please contact support."); + + return; + } + + if (selectedPetIds.length === 0) { + setError(isAdoptionService ? "Please select a pet to adopt." : "Please select at least one pet."); + + return; + } + + setSubmitting(true); + + try { + const body = { + customerId: user.customerId, + storeId: Number(storeId), + serviceId: Number(serviceId), + appointmentDate, + appointmentTime: appointmentTime + ":00", + appointmentStatus: "Booked", + }; + + if (isCustomerPetService) { + body.customerPetIds = selectedPetIds; + } + + else { + body.petIds = selectedPetIds; + } + + const res = await fetch(`${API_BASE}/api/v1/appointments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + + throw new Error(data?.message || data?.error || `Request failed (${res.status})`); + } + + setSuccess("Appointment booked successfully!"); + setStoreId(""); + setServiceId(""); + setAppointmentDate(""); + setAppointmentTime(""); + setSelectedPetIds([]); + setAvailableSlots([]); + loadAppointments(); + } + + catch (err) { + setError(err.message); + } + + finally { + setSubmitting(false); + } + } + + if (authLoading) { + + return ( +
+

Loading...

+
+ ); + } + + if (!user) return null; + + const petsToShow = isAdoptionService ? adoptablePets : isCustomerPetService ? customerPets : []; + const petSectionLabel = isAdoptionService ? "Select a Pet to Adopt" : "Select Pet(s)"; + const noPetsMessage = isAdoptionService + ? "No pets are currently available for adoption." + : "No pets found. Please add your pets in your profile before booking."; + + return ( +
+
+

Schedule an Appointment

+

Book a service for your pet or schedule a pet adoption visit

+
+
+ +
+
+

New Appointment

+ + {error &&
{error}
} + {success &&
{success}
} + + + + + + {selectedService && ( +
+

{selectedService.serviceDesc}

+
+ )} + +
+ Date + +
+ + {storeId && serviceId && appointmentDate && ( +
+ Available Time Slots + {loadingSlots ? ( +

Checking availability...

+ ) : availableSlots.length === 0 ? ( +

No available slots for this date. Please try another date.

+ ) : ( +
+ {availableSlots.map((slot) => ( + + ))} +
+ )} +
+ )} + + {serviceId && ( +
+ {petSectionLabel} + {petsToShow.length === 0 ? ( +

{noPetsMessage}

+ ) : isAdoptionService ? ( +
+ {petsToShow.map((p) => ( + + ))} +
+ ) : ( +
+ {petsToShow.map((p) => ( + + ))} +
+ )} +
+ )} + + +
+ +
+

Your Appointments

+ {loadingAppointments ? ( +

Loading appointments...

+ ) : appointments.length === 0 ? ( +

No appointments yet.

+ ) : ( +
+ {appointments.map((a) => ( +
+
+ {a.serviceName} + + {a.appointmentStatus} + +
+
+ {a.storeName} + {a.appointmentDate} at {formatTime(a.appointmentTime)} +
+ {a.petNames && a.petNames.length > 0 && ( +
+ Pets: {a.petNames.join(", ")} +
+ )} + {a.customerPetNames && a.customerPetNames.length > 0 && ( +
+ Pets: {a.customerPetNames.join(", ")} +
+ )} +
+ ))} +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/web/app/globals.css b/web/app/globals.css index 465c429d..500ccbb0 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -232,7 +232,7 @@ body { border-radius: 2px; } -/* ─── Adopt Page ─────────────────────────────────────────────── */ +/* Adopt page */ .adopt-page { min-height: 100vh; @@ -373,6 +373,12 @@ body { line-height: 1; } +.pet-card-image { + width: 100%; + height: 100%; + object-fit: cover; +} + .pet-card-body { padding: 1rem 1.25rem 1.25rem; display: flex; @@ -459,7 +465,7 @@ body { color: #555; } -/* ─── Pet Detail Page ─────────────────────────────────────────── */ +/* Pet details */ .pet-detail-page { min-height: 100vh; @@ -508,6 +514,12 @@ body { line-height: 1; } +.pet-detail-image { + width: 100%; + height: 100%; + object-fit: cover; +} + .pet-detail-info { flex: 1; padding: 2.5rem 2.5rem 2.5rem 0; @@ -599,7 +611,41 @@ body { background: #e69500; } -/* ─── Responsive Design ──────────────────────────────────────── */ +/* Products Page */ + +.products-page { + min-height: 100vh; +} + +.products-hero { + text-align: center; + padding: 4rem 2rem 3rem; + background: linear-gradient(to bottom, #f9f9f9, #ffffff); +} + +.products-hero-title { + font-size: 3rem; + color: #333; + margin-bottom: 1rem; + font-weight: 700; + letter-spacing: -0.5px; +} + +.products-hero-subtitle { + font-size: 1.5rem; + color: #666; + margin-bottom: 2rem; + font-weight: 300; +} + +.product-card-price { + display: inline-block; + margin-top: 0.4rem; + font-size: 1.05rem; + font-weight: 700; + color: #1a7a3c; +} + /* Responsive Design */ @media (max-width: 1024px) { .adopt-grid { @@ -608,11 +654,13 @@ body { } @media (max-width: 768px) { - .adopt-hero-title { + .adopt-hero-title, + .products-hero-title { font-size: 2rem; } - .adopt-hero-subtitle { + .adopt-hero-subtitle, + .products-hero-subtitle { font-size: 1.2rem; } @@ -641,7 +689,8 @@ body { grid-template-columns: 1fr; } - .adopt-hero-title { + .adopt-hero-title, + .products-hero-title { font-size: 1.6rem; } @@ -705,7 +754,7 @@ body { padding: 2rem 1rem; } } -/* ─── Adopt diagnostic additions ────────────────────────────── */ +/* Adopt diagnostics */ .adopt-controls-row { display: flex; @@ -1020,3 +1069,633 @@ body { color: #888; font-size: 1rem; } + +/* Appointments Page */ + +.appt-page { + min-height: 100vh; +} + +.appt-hero { + text-align: center; + padding: 3rem 2rem 2rem; + background: linear-gradient(135deg, #fff8f0 0%, #fff3e0 100%); +} + +.appt-hero-title { + font-size: 2.2rem; + font-weight: 800; + color: #222; + margin: 0 0 0.5rem; +} + +.appt-hero-subtitle { + font-size: 1.1rem; + color: #666; + margin: 0 0 1rem; +} + +.appt-content { + max-width: 900px; + margin: 0 auto; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.appt-form { + background: white; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.appt-form-title { + font-size: 1.35rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.appt-label { + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.9rem; + font-weight: 600; + color: #444; +} + +.appt-select, +.appt-input { + padding: 0.6rem 0.85rem; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; + background: white; +} + +.appt-select:focus, +.appt-input:focus { + border-color: orange; + box-shadow: 0 0 0 3px rgba(255, 165, 0, 0.2); +} + +.appt-service-info { + background: #fff8f0; + border: 1px solid #ffd180; + border-radius: 8px; + padding: 0.75rem 1rem; + font-size: 0.9rem; + color: #555; +} + +.appt-service-info p { + margin: 0; +} + +.appt-slots-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.appt-slot-btn { + padding: 0.45rem 0.9rem; + border: 1px solid #ddd; + border-radius: 20px; + background: white; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.appt-slot-btn:hover { + border-color: orange; + background: #fff8f0; +} + +.appt-slot-btn--selected { + background: orange; + color: white; + border-color: orange; +} + +.appt-slot-btn--selected:hover { + background: #e69500; +} + +.appt-slots-loading, +.appt-no-slots { + font-size: 0.9rem; + color: #888; + font-weight: 400; + margin: 0.25rem 0 0; +} + +.appt-pets-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.appt-pet-chip { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.4rem 0.85rem; + border: 1px solid #ddd; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; +} + +.appt-pet-chip:hover { + border-color: orange; + background: #fff8f0; +} + +.appt-pet-chip--selected { + background: #fff3e0; + border-color: orange; + color: #c47600; +} + +.appt-pet-chip-species { + font-weight: 400; + color: #888; +} + +.appt-pet-chip--selected .appt-pet-chip-species { + color: #c47600; +} + +.appt-pet-checkbox { + accent-color: orange; +} + +.appt-link { + color: orange; + font-weight: 600; + text-decoration: none; +} + +.appt-link:hover { + text-decoration: underline; +} + +.appt-submit-btn { + margin-top: 0.5rem; + padding: 0.75rem; + background: orange; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; +} + +.appt-submit-btn:hover:not(:disabled) { + background: #e69500; +} + +.appt-submit-btn:active:not(:disabled) { + transform: scale(0.98); +} + +.appt-submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.appt-error { + background: #fff0f0; + border: 1px solid #f5c6c6; + color: #c0392b; + border-radius: 8px; + padding: 0.65rem 1rem; + font-size: 0.9rem; +} + +.appt-success { + background: #f0fff4; + border: 1px solid #b2dfdb; + color: #1a7a3c; + border-radius: 8px; + padding: 0.65rem 1rem; + font-size: 0.9rem; +} + +.appt-loading, +.appt-empty { + text-align: center; + color: #888; + font-size: 0.95rem; + padding: 1rem 0; +} + +.appt-history { + background: white; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); + padding: 2rem; +} + +.appt-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 1rem; +} + +.appt-card { + border: 1px solid #eee; + border-radius: 10px; + padding: 1rem 1.25rem; + transition: box-shadow 0.2s ease; +} + +.appt-card:hover { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); +} + +.appt-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.4rem; +} + +.appt-card-service { + font-weight: 700; + font-size: 1rem; + color: #222; +} + +.appt-card-status { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + padding: 0.2rem 0.7rem; + border-radius: 20px; + letter-spacing: 0.03em; +} + +.appt-card-status--booked { + background: #e3f2fd; + color: #1565c0; +} + +.appt-card-status--completed { + background: #e8f5e9; + color: #2e7d32; +} + +.appt-card-status--cancelled { + background: #fce4ec; + color: #c62828; +} + +.appt-card-details { + display: flex; + justify-content: space-between; + font-size: 0.88rem; + color: #666; +} + +.appt-card-pets { + font-size: 0.85rem; + color: #888; + margin-top: 0.35rem; +} + +/* Adoption Pet Selection */ + +.appt-adopt-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.75rem; + margin-top: 0.25rem; +} + +.appt-adopt-card { + display: flex; + align-items: center; + gap: 0.75rem; + border: 2px solid #eee; + border-radius: 12px; + padding: 0.65rem 0.85rem; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; +} + +.appt-adopt-card:hover { + border-color: orange; + background: #fffaf5; +} + +.appt-adopt-card--selected { + border-color: orange; + background: #fff3e0; +} + +.appt-adopt-radio { + display: none; +} + +.appt-adopt-img { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.appt-adopt-img-placeholder { + width: 48px; + height: 48px; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + flex-shrink: 0; +} + +.appt-adopt-info { + display: flex; + flex-direction: column; + gap: 0.05rem; + min-width: 0; +} + +.appt-adopt-name { + font-weight: 700; + font-size: 0.9rem; + color: #222; +} + +.appt-adopt-detail { + font-size: 0.78rem; + color: #888; +} + +@media (max-width: 640px) { + .appt-content { + padding: 1rem; + } + + .appt-form, + .appt-history { + padding: 1.25rem; + } + + .appt-hero-title { + font-size: 1.6rem; + } + + .appt-card-details { + flex-direction: column; + gap: 0.15rem; + } +} + +/* Profile Page Layout (with pets section) */ + +.profile-page-layout { + min-height: calc(100vh - 70px); + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1rem; + background: #fafafa; + gap: 2rem; +} + +/* Profile Pets Section */ + +.profile-pets-section { + background: white; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); + padding: 2rem; + width: 100%; + max-width: 640px; +} + +.profile-pets-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.profile-pets-title { + font-size: 1.35rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.profile-pets-add-btn { + background: orange; + color: white; + border: none; + border-radius: 20px; + padding: 0.4rem 1rem; + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease; +} + +.profile-pets-add-btn:hover { + background: #e69500; +} + +.profile-pets-empty { + text-align: center; + color: #888; + font-size: 0.95rem; + padding: 1.5rem 0; +} + +.profile-pets-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.profile-pet-card { + display: flex; + align-items: center; + gap: 1rem; + border: 1px solid #eee; + border-radius: 12px; + padding: 0.75rem 1rem; + transition: box-shadow 0.2s ease; +} + +.profile-pet-card:hover { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); +} + +.profile-pet-card-img-area { + position: relative; + width: 56px; + height: 56px; + flex-shrink: 0; +} + +.profile-pet-card-img { + width: 56px; + height: 56px; + border-radius: 50%; + object-fit: cover; +} + +.profile-pet-card-placeholder { + width: 56px; + height: 56px; + border-radius: 50%; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; +} + +.profile-pet-upload-label { + position: absolute; + bottom: -2px; + right: -2px; + width: 22px; + height: 22px; + background: white; + border: 1px solid #ddd; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + cursor: pointer; + transition: border-color 0.2s ease; +} + +.profile-pet-upload-label:hover { + border-color: orange; +} + +.profile-pet-upload-input { + display: none; +} + +.profile-pet-card-info { + display: flex; + flex-direction: column; + gap: 0.1rem; + flex: 1; + min-width: 0; +} + +.profile-pet-card-name { + font-weight: 700; + font-size: 0.95rem; + color: #222; +} + +.profile-pet-card-detail { + font-size: 0.82rem; + color: #888; +} + +.profile-pet-card-actions { + display: flex; + gap: 0.4rem; + flex-shrink: 0; +} + +.profile-pet-edit-btn, +.profile-pet-delete-btn { + padding: 0.3rem 0.7rem; + border-radius: 6px; + font-size: 0.78rem; + font-weight: 600; + cursor: pointer; + border: 1px solid #ddd; + background: white; + transition: all 0.2s ease; +} + +.profile-pet-edit-btn:hover { + border-color: orange; + color: orange; +} + +.profile-pet-delete-btn:hover { + border-color: #c0392b; + color: #c0392b; +} + +/* Pet Add/Edit Form */ + +.profile-pet-form { + background: #fafafa; + border: 1px solid #eee; + border-radius: 12px; + padding: 1.25rem; + margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.profile-pet-form-title { + font-size: 1rem; + font-weight: 700; + color: #222; + margin: 0; +} + +.profile-pet-form-actions { + display: flex; + gap: 0.75rem; +} + +.profile-pet-form-actions .appt-submit-btn { + flex: 1; + margin-top: 0; +} + +.profile-pet-cancel-btn { + flex: 1; + padding: 0.75rem; + background: white; + color: #666; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.profile-pet-cancel-btn:hover { + border-color: #999; + color: #333; +} \ No newline at end of file diff --git a/web/app/products/[id]/page.js b/web/app/products/[id]/page.js new file mode 100644 index 00000000..833cbd24 --- /dev/null +++ b/web/app/products/[id]/page.js @@ -0,0 +1,57 @@ +"use client"; + +import Link from "next/link"; +import { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; +import ProductProfile from "@/components/ProductProfile"; + +const API_BASE = ""; + +export default function ProductDetailPage() { + const { id } = useParams(); + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!id) { + return; + } + + setLoading(true); + setError(null); + + fetch(`${API_BASE}/api/v1/products/${id}`) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP ${res.status} – ${res.statusText}`); + } + + return res.json(); + }) + .then((data) => setProduct(data)) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, [id]); + + return ( +
+
+ ← Back to Products + + {loading &&

Loading product details...

} + {error &&

{error}

} + + {!loading && !error && product && ( + + )} +
+
+ ); +} diff --git a/web/app/products/page.js b/web/app/products/page.js new file mode 100644 index 00000000..f44475f1 --- /dev/null +++ b/web/app/products/page.js @@ -0,0 +1,133 @@ +"use client"; + +import { useState, useEffect } from "react"; +import ProductCard from "@/components/ProductCard"; + +const API_BASE = ""; + +export default function ProductsPage() { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); + const [page, setPage] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + const PAGE_SIZE = 12; + + useEffect(() => { + setLoading(true); + setError(null); + + const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "prodId,asc" }); + if (query) { + params.set("q", query); + } + + fetch(`${API_BASE}/api/v1/products?${params}`) + .then((res) => { + if (!res.ok) { + throw new Error(`HTTP ${res.status} – ${res.statusText}`); + } + + return res.json(); + }) + .then((data) => { + setProducts(data.content ?? []); + setTotalPages(data.totalPages ?? 0); + }) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }, [page, query]); + + function handleSearch(e) { + e.preventDefault(); + setPage(0); + setQuery(search.trim()); + } + + return ( +
+
+

Shop Our Products

+

Everything your pet needs, all in one place

+
+
+ +
+
+
+ setSearch(e.target.value)} + /> + + {query && ( + + )} +
+
+
+ +
+ {loading &&

Loading products...

} + + {error && ( +
+

Failed to load products

+ {error} +
+ )} + + {!loading && !error && products.length === 0 && ( +

No products found.

+ )} + + {!loading && !error && products.length > 0 && ( +
+ {products.map((product) => ( + + ))} +
+ )} + + {!loading && totalPages > 1 && ( +
+ + Page {page + 1} of {totalPages} + +
+ )} +
+
+ ); +} diff --git a/web/app/profile/page.js b/web/app/profile/page.js index fcc903cc..6b28aa35 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -1,25 +1,158 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; +const API_BASE = ""; + export default function ProfilePage() { - const { user, loading, logout } = useAuth(); + const {user, token, loading, logout} = useAuth(); const router = useRouter(); + const [pets, setPets] = useState([]); + const [loadingPets, setLoadingPets] = useState(false); + const [showForm, setShowForm] = useState(false); + const [editingPet, setEditingPet] = useState(null); + const [petName, setPetName] = useState(""); + const [species, setSpecies] = useState(""); + const [breed, setBreed] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [petError, setPetError] = useState(null); + useEffect(() => { if (!loading && !user) { router.replace("/login"); } + }, [user, loading, router]); + const loadPets = useCallback(() => { + if (!token) return; + setLoadingPets(true); + fetch(`${API_BASE}/api/v1/my-pets`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((r) => r.json()) + .then(setPets) + .catch(() => {}) + .finally(() => setLoadingPets(false)); + }, [token]); + + useEffect(() => { + if (user?.role === "CUSTOMER") { + loadPets(); + } + }, [user, loadPets]); + function handleLogout() { logout(); - router.push("/"); } + function openAddForm() { + setEditingPet(null); + setPetName(""); + setSpecies(""); + setBreed(""); + setPetError(null); + setShowForm(true); + } + + function openEditForm(pet) { + setEditingPet(pet); + setPetName(pet.petName); + setSpecies(pet.species); + setBreed(pet.breed || ""); + setPetError(null); + setShowForm(true); + } + + function closeForm() { + setShowForm(false); + setEditingPet(null); + setPetError(null); + } + + async function handlePetSubmit(e) { + e.preventDefault(); + setPetError(null); + setSubmitting(true); + + const url = editingPet + ? `${API_BASE}/api/v1/my-pets/${editingPet.customerPetId}` + : `${API_BASE}/api/v1/my-pets`; + + try { + const res = await fetch(url, { + method: editingPet ? "PUT" : "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ petName, species, breed: breed || null }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + throw new Error(data?.message || `Request failed (${res.status})`); + } + + closeForm(); + loadPets(); + } + + catch (err) { + setPetError(err.message); + } + + finally { + setSubmitting(false); + } + } + + async function handleDeletePet(id) { + if (!confirm("Remove this pet profile?")) { + return; + } + + try { + await fetch(`${API_BASE}/api/v1/my-pets/${id}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + loadPets(); + } + + catch { + } + } + + async function handleImageUpload(petId, file) { + const formData = new FormData(); + formData.append("image", file); + + try { + const res = await fetch(`${API_BASE}/api/v1/my-pets/${petId}/image`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }); + + if (!res.ok) { + const data = await res.json().catch(() => null); + alert(data?.message || "Failed to upload image"); + return; + } + + loadPets(); + } + + catch { + alert("Failed to upload image"); + } + } + if (loading || !user) { return

Loading…

; } @@ -30,11 +163,11 @@ export default function ProfilePage() { {label: "Email", value: user.email}, {label: "Phone", value: user.phone || "—"}, {label: "Role", value: user.role}, - ...(user.storeName ? [{label: "Store", value: user.storeName}] : []), + ...(user.storeName ? [{ label: "Store", value: user.storeName }] : []), ]; return ( -
+
{(user.fullName || user.username).charAt(0).toUpperCase()} @@ -56,6 +189,111 @@ export default function ProfilePage() { Log Out
+ + {user.role === "CUSTOMER" && ( +
+
+

My Pets

+ +
+ + {showForm && ( +
+

+ {editingPet ? "Edit Pet" : "Add a New Pet"} +

+ {petError &&
{petError}
} + + + +
+ + +
+
+ )} + + {loadingPets ? ( +

Loading pets...

+ ) : pets.length === 0 && !showForm ? ( +

No pet profiles yet. Add your first pet above!

+ ) : ( +
+ {pets.map((pet) => ( +
+
+ {pet.imageUrl ? ( + {pet.petName} + ) : ( +
🐾
+ )} + +
+
+ {pet.petName} + {pet.species} + {pet.breed && {pet.breed}} +
+
+ + +
+
+ ))} +
+ )} +
+ )}
); } diff --git a/web/components/Navigation.js b/web/components/Navigation.js index 2e7f7cac..cfd17924 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -25,7 +25,7 @@ export default function DisplayNav() {
Home Adopt a Pet - Online Store + Online Store Schedule an Appointment Contact Us About Us diff --git a/web/components/PetCard.js b/web/components/PetCard.js index 5ccb0219..4370bca1 100644 --- a/web/components/PetCard.js +++ b/web/components/PetCard.js @@ -1,13 +1,11 @@ -//Pet cards (on adopt page) - import Link from "next/link"; -import { getSpeciesEmoji, getStatusClass } from "@/components/petUtils"; +import { getStatusClass } from "@/components/petUtils"; -export default function PetCard({petId, petName, petSpecies, petStatus}) { +export default function PetCard({petId, petName, petSpecies, petStatus, imageUrl}) { return (
- {getSpeciesEmoji(petSpecies)} + {petName}

{petName}

diff --git a/web/components/PetProfile.js b/web/components/PetProfile.js index 87d4b059..a00732a4 100644 --- a/web/components/PetProfile.js +++ b/web/components/PetProfile.js @@ -1,11 +1,11 @@ import Link from "next/link"; -import { getSpeciesEmoji, getStatusClass } from "@/components/petUtils"; +import { getStatusClass } from "@/components/petUtils"; -export default function PetProfile({ petName, petSpecies, petBreed, petAge, petStatus, petPrice }) { +export default function PetProfile({ petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl }) { return (
- {getSpeciesEmoji(petSpecies)} + {petName}
@@ -45,7 +45,7 @@ export default function PetProfile({ petName, petSpecies, petBreed, petAge, petS

Interested in adopting {petName}? Visit us in store or schedule an appointment.

- + Schedule an Appointment
diff --git a/web/components/ProductCard.js b/web/components/ProductCard.js new file mode 100644 index 00000000..e27abbbd --- /dev/null +++ b/web/components/ProductCard.js @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function ProductCard({ prodId, prodName, categoryName, prodPrice, imageUrl }) { + return ( + +
+ {prodName} +
+
+

{prodName}

+

{categoryName}

+ {prodPrice != null && ( + ${parseFloat(prodPrice).toFixed(2)} + )} +
+ + ); +} diff --git a/web/components/ProductProfile.js b/web/components/ProductProfile.js new file mode 100644 index 00000000..b8d0a9b2 --- /dev/null +++ b/web/components/ProductProfile.js @@ -0,0 +1,34 @@ +import Link from "next/link"; + +export default function ProductProfile({ prodName, categoryName, prodDesc, prodPrice, imageUrl }) { + return ( +
+
+ {prodName} +
+ +
+
+

{prodName}

+
+ +
+
+ Category + {categoryName ?? "—"} +
+
+ Price + + {prodPrice != null ? `$${parseFloat(prodPrice).toFixed(2)}` : "—"} + +
+
+ Description + {prodDesc ?? "—"} +
+
+
+
+ ); +} diff --git a/web/public/images/home/navimages/about.jpg b/web/public/images/home/navimages/about.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64537455341056ecc00211b4eba95f2bba83b54d GIT binary patch literal 251455 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%0A=Rb|X$Ks?yXuX2+1+;S#q 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

NAFM|qBWq5P9YQ_X2=34EjT149+ERt1$-+j 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 Eg3hk)xc~qF literal 0 HcmV?d00001 From 33c9555564ad2e4ec80e9014f4c4af5df6c1a179 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:16:52 -0600 Subject: [PATCH 025/137] Polish sales tables --- .../controllers/AdoptionController.java | 3 + .../controllers/AppointmentController.java | 3 + .../controllers/PurchaseOrderController.java | 4 +- .../controllers/SaleController.java | 216 ++++++++++++++---- .../controllers/StaffAccountsController.java | 2 + .../SaleDetailDialogController.java | 54 +++++ .../dialogviews/sale-detail-dialog-view.fxml | 61 +++++ .../petshopdesktop/modelviews/sale-view.fxml | 22 +- 8 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java create mode 100644 desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 65edcebd..564e6205 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -18,6 +18,7 @@ import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.util.ActivityLogger; import java.io.IOException; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -168,6 +169,7 @@ public class AdoptionController { List adoptions = AdoptionApi.getInstance().listAdoptions(filter); List adoptionList = adoptions.stream() .map(this::mapToAdoption) + .sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -193,6 +195,7 @@ public class AdoptionController { List adoptions = AdoptionApi.getInstance().listAdoptions(null); List adoptionList = adoptions.stream() .map(this::mapToAdoption) + .sorted(Comparator.comparing(Adoption::getAdoptionDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 221140b0..bd5dc392 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -20,6 +20,7 @@ import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialo import org.example.petshopdesktop.util.ActivityLogger; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class AppointmentController { @@ -81,6 +82,7 @@ public class AppointmentController { List responses = AppointmentApi.getInstance().listAppointments(null); List appointmentDTOs = responses.stream() .map(this::mapToAppointmentDTO) + .sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -105,6 +107,7 @@ public class AppointmentController { List responses = AppointmentApi.getInstance().listAppointments(query); List appointmentDTOs = responses.stream() .map(this::mapToAppointmentDTO) + .sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java index 71ec81ec..d8ea54b6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java @@ -13,6 +13,7 @@ import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi; import org.example.petshopdesktop.util.ActivityLogger; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class PurchaseOrderController { @@ -63,6 +64,7 @@ public class PurchaseOrderController { List responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null); List dtos = responses.stream() .map(this::mapToPurchaseOrderDTO) + .sorted(Comparator.comparing(PurchaseOrderDTO::getOrderDate).reversed()) .collect(Collectors.toList()); Platform.runLater(() -> { @@ -118,4 +120,4 @@ public class PurchaseOrderController { response.getOrderStatus() ); } -} \ No newline at end of file +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index c3866b1e..7e80c531 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -6,6 +6,8 @@ import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; +import javafx.application.Platform; +import javafx.scene.input.MouseButton; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; @@ -32,6 +34,7 @@ import org.example.petshopdesktop.api.dto.sale.SaleRequest; import org.example.petshopdesktop.api.dto.sale.SaleResponse; import org.example.petshopdesktop.models.Product; import org.example.petshopdesktop.models.SaleCartItem; +import org.example.petshopdesktop.models.SaleDetail; import org.example.petshopdesktop.models.SaleLineItem; import org.example.petshopdesktop.util.ActivityLogger; @@ -39,6 +42,7 @@ import java.math.BigDecimal; import java.text.NumberFormat; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; @@ -128,6 +132,7 @@ public class SaleController { private final ObservableList cartItems = FXCollections.observableArrayList(); private final ObservableList saleItems = FXCollections.observableArrayList(); private FilteredList filteredSales; + private boolean saleSaveInProgress; private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @@ -165,6 +170,15 @@ public class SaleController { filteredSales = new FilteredList<>(saleItems, s -> true); tvSales.setItems(filteredSales); + tvSales.setOnMouseClicked(event -> { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + SaleLineItem selected = tvSales.getSelectionModel().getSelectedItem(); + if (selected != null) { + openSaleDetailDialog(selected.getSaleId()); + } + } + }); + txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal)); } @@ -177,22 +191,43 @@ public class SaleController { updateCartTotal(); - try { - List productResponses = ProductApi.getInstance().listProducts(null); - ObservableList products = FXCollections.observableArrayList(); - for (ProductResponse pr : productResponses) { - products.add(new Product( - pr.getProdId().intValue(), - pr.getProdName(), - pr.getProdPrice().doubleValue(), - 0, - pr.getProdDesc() - )); + setCreateSaleControlsDisabled(true); + + Task> task = new Task<>() { + @Override + protected ObservableList call() throws Exception { + List productResponses = ProductApi.getInstance().listProducts(null); + ObservableList products = FXCollections.observableArrayList(); + for (ProductResponse pr : productResponses) { + products.add(new Product( + pr.getProdId().intValue(), + pr.getProdName(), + pr.getProdPrice().doubleValue(), + 0, + pr.getProdDesc() + )); + } + return products; } - cbProduct.setItems(products); - } catch (Exception e) { - ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Loading products"); - } + }; + + task.setOnSucceeded(event -> { + cbProduct.setItems(task.getValue()); + setCreateSaleControlsDisabled(false); + }); + + task.setOnFailed(event -> { + Throwable e = task.getException(); + ActivityLogger.getInstance().logException( + "SaleController.setupCreateSale", + e instanceof Exception ? (Exception) e : new RuntimeException(e), + "Loading products" + ); + setCreateSaleControlsDisabled(false); + showError("Sales", "Could not load products. Check the backend connection and refresh the view."); + }); + + new Thread(task).start(); } private void applyRoleMode() { @@ -207,10 +242,13 @@ public class SaleController { } private void refreshSales(boolean showErrorDialog) { + btnRefresh.setDisable(true); Task> task = new Task>() { @Override protected List call() throws Exception { - List sales = SaleApi.getInstance().listSales(0, 1000, null); + List sales = SaleApi.getInstance().listAllSales(null); + sales.sort(Comparator.comparing(SaleResponse::getSaleDate, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(SaleResponse::getSaleId, Comparator.nullsLast(Comparator.reverseOrder()))); List lineItems = new ArrayList<>(); for (SaleResponse sale : sales) { @@ -242,14 +280,14 @@ public class SaleController { task.setOnSucceeded(event -> { saleItems.setAll(task.getValue()); + btnRefresh.setDisable(false); }); task.setOnFailed(event -> { Throwable e = task.getException(); ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales"); - if (showErrorDialog) { - showError("Sales", "Could not load sales: " + e.getMessage()); - } + btnRefresh.setDisable(false); + showError("Sales", "Could not load sales: " + e.getMessage()); }); new Thread(task).start(); @@ -310,6 +348,9 @@ public class SaleController { @FXML void btnSaveSale(ActionEvent event) { + if (saleSaveInProgress) { + return; + } if (UserSession.getInstance().isAdmin()) { showError("Create Sale", "This action is restricted to staff."); return; @@ -332,36 +373,57 @@ public class SaleController { return; } - try { - SaleRequest request = new SaleRequest(); - request.setStoreId(storeId); - request.setPaymentMethod(payment); + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); - List itemRequests = new ArrayList<>(); - for (SaleCartItem cartItem : cartItems) { - SaleItemRequest itemRequest = new SaleItemRequest(); - itemRequest.setProdId((long) cartItem.getProdId()); - itemRequest.setQuantity(cartItem.getQuantity()); - itemRequests.add(itemRequest); + List itemRequests = new ArrayList<>(); + for (SaleCartItem cartItem : cartItems) { + SaleItemRequest itemRequest = new SaleItemRequest(); + itemRequest.setProdId((long) cartItem.getProdId()); + itemRequest.setQuantity(cartItem.getQuantity()); + itemRequests.add(itemRequest); + } + request.setItems(itemRequests); + + saleSaveInProgress = true; + setCreateSaleControlsDisabled(true); + btnRefund.setDisable(true); + + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().createSale(request); } - request.setItems(itemRequests); + }; - SaleResponse response = SaleApi.getInstance().createSale(request); + task.setOnSucceeded(evt -> { + saleSaveInProgress = false; + setCreateSaleControlsDisabled(false); + btnRefund.setDisable(false); + SaleResponse response = task.getValue(); showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created."); - cartItems.clear(); updateCartTotal(); - refreshSales(true); - } catch (Exception e) { - ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Creating sale"); - String errorMsg = e.getMessage(); + }); + + task.setOnFailed(evt -> { + saleSaveInProgress = false; + setCreateSaleControlsDisabled(false); + btnRefund.setDisable(false); + Throwable e = task.getException(); + Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e); + ActivityLogger.getInstance().logException("SaleController.btnSaveSale", ex, "Creating sale"); + String errorMsg = e != null ? e.getMessage() : null; if (errorMsg != null && errorMsg.contains("Insufficient inventory")) { showError("Create Sale", "Insufficient stock for one or more items."); } else { showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale."); } - } + }); + + new Thread(task).start(); } @FXML @@ -384,11 +446,13 @@ public class SaleController { dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle("Process Refund"); dialog.setScene(new Scene(loader.load())); + var controller = loader.getController(); if (selectedSale != null) { - loader.getController() - .prefillSale((long) selectedSale.getSaleId()); + controller.prefillSale((long) selectedSale.getSaleId()); } - dialog.setResizable(false); + dialog.setMinWidth(860); + dialog.setMinHeight(680); + dialog.setResizable(true); dialog.showAndWait(); refreshSales(true); @@ -397,11 +461,83 @@ public class SaleController { } } + private void openSaleDetailDialog(int saleId) { + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().getSale((long) saleId); + } + }; + + task.setOnSucceeded(event -> { + try { + SaleResponse sale = task.getValue(); + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvSales.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Sale Details"); + dialog.setScene(new Scene(loader.load())); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.SaleDetailDialogController) loader.getController(); + controller.displaySaleDetails(mapToSaleDetail(sale)); + dialog.setResizable(false); + dialog.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleController.openSaleDetailDialog", e, "Opening sale detail dialog"); + showError("Sale Details", "Could not open the sale details."); + } + }); + + task.setOnFailed(event -> { + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("SaleController.openSaleDetailDialog", (Exception) e, "Loading sale detail"); + showError("Sale Details", "Could not open the sale details."); + }); + + new Thread(task).start(); + } + + private SaleDetail mapToSaleDetail(SaleResponse sale) { + ObservableList items = FXCollections.observableArrayList(); + if (sale.getItems() != null) { + for (SaleItemResponse item : sale.getItems()) { + double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; + int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + items.add(new SaleDetail.SaleDetailItem( + item.getProdId() != null ? item.getProdId().intValue() : 0, + item.getProductName(), + quantity, + unitPrice, + unitPrice * quantity + )); + } + } + return new SaleDetail( + sale.getSaleId().intValue(), + sale.getSaleDate(), + sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0, + sale.getPaymentMethod(), + sale.getEmployeeName(), + items + ); + } + private void updateCartTotal() { double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); lblCartTotal.setText(currency.format(total)); } + private void setCreateSaleControlsDisabled(boolean disabled) { + cbProduct.setDisable(disabled); + spQuantity.setDisable(disabled); + btnAddToCart.setDisable(disabled); + btnRemoveSelected.setDisable(disabled); + cbPaymentMethod.setDisable(disabled); + btnClearCart.setDisable(disabled); + btnSaveSale.setDisable(disabled); + } + private void applySalesFilter(String filter) { String f = filter == null ? "" : filter.trim().toLowerCase(); if (f.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index a02674e4..603b85fa 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -25,6 +25,7 @@ import org.example.petshopdesktop.util.ActivityLogger; import java.sql.Timestamp; import java.time.ZoneId; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; public class StaffAccountsController { @@ -161,6 +162,7 @@ public class StaffAccountsController { List employees = EmployeeApi.getInstance().listEmployees(null); List accounts = employees.stream() .map(this::mapToStaffAccount) + .sorted(Comparator.comparing(StaffAccount::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()))) .collect(Collectors.toList()); Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java new file mode 100644 index 00000000..3a0c67cc --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SaleDetailDialogController.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Stage; +import org.example.petshopdesktop.models.SaleDetail; + +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class SaleDetailDialogController { + + @FXML private Label lblSaleId; + @FXML private Label lblSaleDate; + @FXML private Label lblEmployee; + @FXML private Label lblPayment; + @FXML private Label lblTotal; + @FXML private TableView tvItems; + @FXML private TableColumn colProduct; + @FXML private TableColumn colQuantity; + @FXML private TableColumn colUnitPrice; + @FXML private TableColumn colLineTotal; + + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + @FXML + public void initialize() { + tvItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); + colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colLineTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + } + + public void displaySaleDetails(SaleDetail sale) { + lblSaleId.setText(String.valueOf(sale.getSaleId())); + lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : ""); + lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : ""); + lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : ""); + lblTotal.setText(currency.format(sale.getTotalAmount())); + tvItems.setItems(sale.getItems()); + } + + @FXML + void btnCloseClicked() { + Stage stage = (Stage) tvItems.getScene().getWindow(); + stage.close(); + } +} diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml new file mode 100644 index 00000000..a0964b27 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/sale-detail-dialog-view.fxml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -151,16 +151,16 @@ - + - - - - - - - - + + + + + + + + From a3851871c7d3026251ed83a55a38a3e44e78d531 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:17:22 -0600 Subject: [PATCH 026/137] Stabilize refunds --- .../api/dto/common/PageResponse.java | 66 ++++++- .../petshopdesktop/api/endpoints/SaleApi.java | 33 ++++ .../RefundDialogController.java | 162 ++++++++++++------ .../dialogviews/refund-dialog-view.fxml | 62 +++---- 4 files changed, 236 insertions(+), 87 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java index bbcc467c..5a27e2e9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java @@ -9,15 +9,14 @@ import java.util.List; public class PageResponse { private List content; - @JsonProperty("number") private int pageNumber; - @JsonProperty("size") private int pageSize; private long totalElements; private int totalPages; private boolean last; + private PageMetadata page; public PageResponse() { } @@ -63,10 +62,71 @@ public class PageResponse { } public boolean isLast() { - return last; + if (last) { + return true; + } + if (page != null) { + return page.number >= Math.max(0, page.totalPages - 1); + } + return content == null || content.isEmpty(); } public void setLast(boolean last) { this.last = last; } + + public PageMetadata getPage() { + return page; + } + + public void setPage(PageMetadata page) { + this.page = page; + if (page != null) { + this.pageNumber = page.number; + this.pageSize = page.size; + this.totalElements = page.totalElements; + this.totalPages = page.totalPages; + this.last = page.number >= Math.max(0, page.totalPages - 1); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class PageMetadata { + private int size; + private int number; + private long totalElements; + private int totalPages; + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public int getTotalPages() { + return totalPages; + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java index d3355012..a087fd9f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java @@ -38,6 +38,39 @@ public class SaleApi { return pageResponse.getContent(); } + public List listAllSales(String query) throws Exception { + int page = 0; + int size = 250; + List allSales = new java.util.ArrayList<>(); + + while (true) { + String path = "/api/v1/sales?page=" + page + "&size=" + size; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from sales endpoint"); + } + + if (pageResponse.getContent() != null) { + allSales.addAll(pageResponse.getContent()); + } + + if (pageResponse.isLast()) { + break; + } + page++; + } + + return allSales; + } + public SaleResponse getSale(Long id) throws Exception { return apiClient.get("/api/v1/sales/" + id, SaleResponse.class); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java index 860a9921..bc262e30 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -4,8 +4,12 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.concurrent.Task; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringWrapper; import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; +import javafx.application.Platform; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; @@ -90,6 +94,7 @@ public class RefundDialogController { private final ObservableList originalItems = FXCollections.observableArrayList(); private final ObservableList refundItems = FXCollections.observableArrayList(); private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private boolean refundInProgress; @FXML public void initialize() { @@ -100,17 +105,19 @@ public class RefundDialogController { } private void setupTables() { - colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal")); + tvOriginalItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colOriginalProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colOriginalQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colOriginalUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colOriginalTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getLineTotal())); tvOriginalItems.setItems(originalItems); tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); - colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); - colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); - colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvRefundItems.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + colRefundProduct.setCellValueFactory(cell -> new ReadOnlyStringWrapper(cell.getValue().getProductName())); + colRefundQuantity.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getQuantity())); + colRefundUnitPrice.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getUnitPrice())); + colRefundTotal.setCellValueFactory(cell -> new ReadOnlyObjectWrapper<>(cell.getValue().getTotal())); tvRefundItems.setItems(refundItems); tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); } @@ -125,12 +132,13 @@ public class RefundDialogController { return; } txtSaleId.setText(String.valueOf(saleId)); - loadSale(); + Platform.runLater(this::loadSale); } private void loadSale() { String saleIdText = txtSaleId.getText().trim(); if (saleIdText.isEmpty()) { + clearLoadedSale(); showError("Load Sale", "Enter a transaction ID."); return; } @@ -139,22 +147,36 @@ public class RefundDialogController { try { saleId = Long.parseLong(saleIdText); } catch (NumberFormatException e) { + clearLoadedSale(); showError("Load Sale", "Invalid transaction ID."); return; } - try { - List allSales = SaleApi.getInstance().listSales(0, 1000, null); - currentSale = SaleApi.getInstance().getSale(saleId); - if (Boolean.TRUE.equals(currentSale.getIsRefund())) { - clearLoadedSale(); - showError("Load Sale", "Select an original sale, not a refund record."); - return; - } - List previousRefunds = allSales.stream() - .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) - .collect(Collectors.toList()); + setLoadingState(true, "Loading sale..."); + clearLoadedSale(); + Task task = new Task<>() { + @Override + protected LoadedSaleData call() throws Exception { + List allSales = SaleApi.getInstance().listAllSales(null); + SaleResponse sale = SaleApi.getInstance().getSale(saleId); + if (Boolean.TRUE.equals(sale.getIsRefund())) { + throw new IllegalStateException("Select an original sale, not a refund record."); + } + List previousRefunds = allSales.stream() + .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) + .collect(Collectors.toList()); + List refundableItems = buildRefundableItems(sale, previousRefunds); + if (refundableItems.isEmpty()) { + throw new IllegalStateException("This sale has no remaining refundable items."); + } + return new LoadedSaleData(sale, refundableItems); + } + }; + + task.setOnSucceeded(event -> { + LoadedSaleData loaded = task.getValue(); + currentSale = loaded.sale(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s", currentSale.getSaleDate().format(formatter), @@ -162,26 +184,25 @@ public class RefundDialogController { currency.format(currentSale.getTotalAmount()), currentSale.getPaymentMethod()); lblSaleInfo.setText(saleInfo); - - List refundableItems = buildRefundableItems(currentSale, previousRefunds); - if (refundableItems.isEmpty()) { - showError("Load Sale", "This sale has no remaining refundable items."); - return; - } - baseOriginalItems.clear(); - baseOriginalItems.addAll(copySaleItems(refundableItems)); - originalItems.setAll(copySaleItems(refundableItems)); + baseOriginalItems.addAll(copySaleItems(loaded.refundableItems())); + originalItems.setAll(copySaleItems(loaded.refundableItems())); cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod()); - refundItems.clear(); updateOriginalItemAvailability(); updateRefundTotal(); + setLoadingState(false, saleInfo); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale"); + task.setOnFailed(event -> { + clearLoadedSale(); + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", (Exception) e, "Loading sale"); + setLoadingState(false, ""); showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale."); - } + }); + + new Thread(task).start(); } @FXML @@ -248,6 +269,9 @@ public class RefundDialogController { @FXML void btnProcessRefundClicked(ActionEvent event) { + if (refundInProgress) { + return; + } if (currentSale == null) { showError("Process Refund", "Load a sale first."); return; @@ -280,36 +304,53 @@ public class RefundDialogController { return; } - try { - SaleRequest request = new SaleRequest(); - request.setStoreId(storeId); - request.setPaymentMethod(payment); - request.setIsRefund(true); - request.setOriginalSaleId(currentSale.getSaleId()); + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); + request.setIsRefund(true); + request.setOriginalSaleId(currentSale.getSaleId()); - List items = new ArrayList<>(); - for (RefundItem item : refundItems) { - SaleItemRequest saleItem = new SaleItemRequest(); - saleItem.setProdId((long) item.getProdId()); - saleItem.setQuantity(-item.getQuantity()); - items.add(saleItem); + List items = new ArrayList<>(); + for (RefundItem item : refundItems) { + SaleItemRequest saleItem = new SaleItemRequest(); + saleItem.setProdId((long) item.getProdId()); + saleItem.setQuantity(-item.getQuantity()); + items.add(saleItem); + } + request.setItems(items); + + refundInProgress = true; + setLoadingState(true, lblSaleInfo.getText()); + + Task task = new Task<>() { + @Override + protected SaleResponse call() throws Exception { + return SaleApi.getInstance().createSale(request); } - request.setItems(items); - - SaleResponse refundResponse = SaleApi.getInstance().createSale(request); + }; + task.setOnSucceeded(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + SaleResponse refundResponse = task.getValue(); Alert success = new Alert(Alert.AlertType.INFORMATION); success.setTitle("Refund Processed"); success.setHeaderText(null); success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully."); success.showAndWait(); - closeDialog(); + }); - } catch (Exception e) { - ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund"); - showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund."); - } + task.setOnFailed(evt -> { + refundInProgress = false; + setLoadingState(false, lblSaleInfo.getText()); + Throwable e = task.getException(); + Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e); + ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", ex, "Processing refund"); + showError("Process Refund", e != null && e.getMessage() != null ? e.getMessage() : "Could not process refund."); + }); + + new Thread(task).start(); } @FXML @@ -326,6 +367,18 @@ public class RefundDialogController { updateRefundTotal(); } + private void setLoadingState(boolean loading, String message) { + btnLoadSale.setDisable(loading); + btnAddToRefund.setDisable(loading); + btnRemoveFromRefund.setDisable(loading); + btnProcessRefund.setDisable(loading); + txtSaleId.setDisable(loading); + cbPaymentMethod.setDisable(loading); + if (loading) { + lblSaleInfo.setText(message); + } + } + private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) { for (int i = 0; i < refundItems.size(); i++) { RefundItem existing = refundItems.get(i); @@ -486,4 +539,7 @@ public class RefundDialogController { return quantity * unitPrice; } } + + private record LoadedSaleData(SaleResponse sale, List refundableItems) { + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml index b349a1d9..aad2f4c7 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml @@ -13,7 +13,7 @@ - + @@ -84,14 +84,14 @@ - - - - - - - - + From 4ef913dfd025a3a776bfac723f0ebe24dd0bef1a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:40:22 -0600 Subject: [PATCH 027/137] Fix refund display --- .../controllers/SaleController.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java index 7e80c531..22c96dbd 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -258,18 +258,23 @@ public class SaleController { if (sale.getItems() != null && !sale.getItems().isEmpty()) { for (SaleItemResponse item : sale.getItems()) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; - double lineTotal = unitPrice * item.getQuantity(); + int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } + double lineTotal = unitPrice * quantity; lineItems.add(new SaleLineItem( sale.getSaleId().intValue(), saleDate, sale.getEmployeeName(), item.getProductName(), - item.getQuantity(), + quantity, unitPrice, lineTotal, sale.getPaymentMethod(), - sale.getIsRefund() != null && sale.getIsRefund() + isRefund )); } } @@ -501,9 +506,13 @@ public class SaleController { private SaleDetail mapToSaleDetail(SaleResponse sale) { ObservableList items = FXCollections.observableArrayList(); if (sale.getItems() != null) { + boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund(); for (SaleItemResponse item : sale.getItems()) { double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; int quantity = item.getQuantity() != null ? item.getQuantity() : 0; + if (isRefund && quantity > 0) { + quantity = -quantity; + } items.add(new SaleDetail.SaleDetailItem( item.getProdId() != null ? item.getProdId().intValue() : 0, item.getProductName(), From 2a0f5e760c00999c6b626749a058ed40e3f48443 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:57:46 -0600 Subject: [PATCH 028/137] Fix migration versions --- ...backfill_user_accounts.sql => V12__backfill_user_accounts.sql} | 0 ...it_payment_method.sql => V13__remove_debit_payment_method.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/{V9__backfill_user_accounts.sql => V12__backfill_user_accounts.sql} (100%) rename backend/src/main/resources/db/migration/{V10__remove_debit_payment_method.sql => V13__remove_debit_payment_method.sql} (100%) diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql similarity index 100% rename from backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql rename to backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql diff --git a/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql b/backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql similarity index 100% rename from backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql rename to backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql From 6f0eab23ee0f5791a3427797e2aeed9edd8c53b5 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 30 Mar 2026 09:58:02 -0600 Subject: [PATCH 029/137] Revert "Merge pull request #55 from RecentRunner/backend-normalize-users-payments" This reverts commit 5bd836719b3f321977d94d75ed3bfca3d64de8bd, reversing changes made to f944124972f3242de9db25db4f553a5342f488f1. --- .../backend/service/CustomerService.java | 66 +------ .../petshop/backend/service/SaleService.java | 20 +- .../V10__remove_debit_payment_method.sql | 3 - .../resources/db/migration/V2__seed_data.sql | 6 +- .../migration/V9__backfill_user_accounts.sql | 91 --------- .../backend/service/CustomerServiceTest.java | 180 ------------------ .../backend/service/SaleServiceTest.java | 17 -- 7 files changed, 9 insertions(+), 374 deletions(-) delete mode 100644 backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql delete mode 100644 backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql delete mode 100644 backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java delete mode 100644 backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index 33c731d1..040be22a 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -4,34 +4,23 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.customer.CustomerRequest; import com.petshop.backend.dto.customer.CustomerResponse; import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; - -import static org.springframework.http.HttpStatus.CONFLICT; @Service public class CustomerService { - private static final String TEMP_PASSWORD = "TempPass123!"; - private final CustomerRepository customerRepository; private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; - public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { + public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) { this.customerRepository = customerRepository; this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; } public Page getAllCustomers(String query, Pageable pageable) { @@ -52,19 +41,14 @@ public class CustomerService { @Transactional public CustomerResponse createCustomer(CustomerRequest request) { - ensureEmailAvailable(request.getEmail(), null); - Customer customer = new Customer(); customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - User user = createLinkedUser(customer); - - Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); - syncLinkedUser(linkedCustomer); - return mapToResponse(linkedCustomer); + syncLinkedUser(customer); + return mapToResponse(customer); } @Transactional @@ -72,8 +56,6 @@ public class CustomerService { Customer customer = customerRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); - ensureEmailAvailable(request.getEmail(), customer.getUserId()); - customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); @@ -85,14 +67,9 @@ public class CustomerService { @Transactional public void deleteCustomer(Long id) { - Customer customer = customerRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); - - if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) { - userRepository.deleteById(customer.getUserId()); - return; + if (!customerRepository.existsById(id)) { + throw new ResourceNotFoundException("Customer not found with id: " + id); } - customerRepository.deleteById(id); } @@ -122,37 +99,4 @@ public class CustomerService { userRepository.save(user); }); } - - private User createLinkedUser(Customer customer) { - User user = new User(); - user.setUsername(generateUsername(customer)); - user.setPassword(passwordEncoder.encode(TEMP_PASSWORD)); - user.setEmail(customer.getEmail()); - user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); - user.setPhone(generatePhone(customer)); - user.setRole(User.Role.CUSTOMER); - user.setActive(false); - user.setTokenVersion(0); - return userRepository.save(user); - } - - private String generateUsername(Customer customer) { - return "customer_" + customer.getCustomerId(); - } - - private String generatePhone(Customer customer) { - return String.format("200-000-%04d", customer.getCustomerId()); - } - - private void ensureEmailAvailable(String email, Long currentUserId) { - if (email == null || email.isBlank()) { - return; - } - - userRepository.findByEmail(email).ifPresent(existing -> { - if (currentUserId == null || !existing.getId().equals(currentUserId)) { - throw new ResponseStatusException(CONFLICT, "Email already exists"); - } - }); - } } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b8d5861e..b426dc38 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -78,7 +78,7 @@ public class SaleService { sale.setSaleDate(LocalDateTime.now()); sale.setEmployee(employee); sale.setStore(store); - sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); + sale.setPaymentMethod(request.getPaymentMethod()); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); if (request.getCustomerId() != null) { @@ -215,22 +215,4 @@ public class SaleService { return response; } - - String normalizePaymentMethod(String paymentMethod) { - if (paymentMethod == null) { - return null; - } - - String normalized = paymentMethod.trim(); - if (normalized.equalsIgnoreCase("Debit")) { - return "Card"; - } - if (normalized.equalsIgnoreCase("Cash")) { - return "Cash"; - } - if (normalized.equalsIgnoreCase("Card")) { - return "Card"; - } - return normalized; - } } diff --git a/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql b/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql deleted file mode 100644 index 874b0205..00000000 --- a/backend/src/main/resources/db/migration/V10__remove_debit_payment_method.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE sale -SET paymentMethod = 'Card' -WHERE LOWER(paymentMethod) = 'debit'; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql index d7308ad4..5e8d3fb6 100644 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -128,7 +128,7 @@ VALUES ('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1), ('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2), ('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3), -('2026-01-15 10:45:00', 150.00, 'Card', 1, 1, 1), +('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1), ('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2), ('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL), ('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4), @@ -136,12 +136,12 @@ VALUES ('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1), ('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3), ('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL), -('2026-02-08 16:15:00', 160.00, 'Card', 1, 1, 2), +('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2), ('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL), ('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1), ('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL), ('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4), -('2026-02-20 14:35:00', 155.00, 'Card', 4, 3, NULL), +('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL), ('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2), ('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL), (NOW(), 95.00, 'Card', 1, 1, 1); diff --git a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql deleted file mode 100644 index 4af69669..00000000 --- a/backend/src/main/resources/db/migration/V9__backfill_user_accounts.sql +++ /dev/null @@ -1,91 +0,0 @@ -INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) -SELECT - CONCAT('customer_', c.customerId) AS username, - '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN c.email IS NOT NULL - AND c.email <> '' - AND (SELECT COUNT(*) FROM customer c2 WHERE c2.email = c.email) = 1 - AND NOT EXISTS (SELECT 1 FROM employee e2 WHERE e2.email = c.email) - AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) - THEN c.email - ELSE CONCAT('customer_', c.customerId, '@petshop.local') - END AS email, - CONCAT(c.firstName, ' ', c.lastName) AS fullName, - CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, - 'CUSTOMER' AS role, - FALSE AS active, - 0 AS tokenVersion -FROM customer c -WHERE c.user_id IS NULL - AND NOT EXISTS ( - SELECT 1 - FROM users u - WHERE u.username = CONCAT('customer_', c.customerId) - ); - -INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) -SELECT - CONCAT('employee_', e.employeeId) AS username, - '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN e.email IS NOT NULL - AND e.email <> '' - AND (SELECT COUNT(*) FROM employee e2 WHERE e2.email = e.email) = 1 - AND NOT EXISTS (SELECT 1 FROM customer c2 WHERE c2.email = e.email) - AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) - THEN e.email - ELSE CONCAT('employee_', e.employeeId, '@petshop.local') - END AS email, - CONCAT(e.firstName, ' ', e.lastName) AS fullName, - CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, - CASE - WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' - ELSE 'STAFF' - END AS role, - FALSE AS active, - 0 AS tokenVersion -FROM employee e -WHERE e.user_id IS NULL - AND NOT EXISTS ( - SELECT 1 - FROM users u - WHERE u.username = CONCAT('employee_', e.employeeId) - ); - -UPDATE customer c -JOIN users u ON u.username = CONCAT('customer_', c.customerId) - AND u.role = 'CUSTOMER' -SET c.user_id = u.id -WHERE c.user_id IS NULL; - -UPDATE employee e -JOIN users u ON u.username = CONCAT('employee_', e.employeeId) - AND u.role IN ('STAFF', 'ADMIN') -SET e.user_id = u.id -WHERE e.user_id IS NULL; - -UPDATE users -SET - fullName = CASE - WHEN fullName IS NULL OR fullName = '' THEN username - ELSE fullName - END, - email = CASE - WHEN email IS NULL OR email = '' THEN CONCAT(username, '@petshop.local') - ELSE email - END, - phone = CASE - WHEN phone IS NULL OR phone = '' THEN CONCAT('000-000-', LPAD(id, 4, '0')) - ELSE phone - END, - active = COALESCE(active, TRUE), - tokenVersion = COALESCE(tokenVersion, 0) -WHERE fullName IS NULL - OR fullName = '' - OR email IS NULL - OR email = '' - OR phone IS NULL - OR phone = '' - OR active IS NULL - OR tokenVersion IS NULL; diff --git a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java b/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java deleted file mode 100644 index dde07768..00000000 --- a/backend/src/test/java/com/petshop/backend/service/CustomerServiceTest.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.customer.CustomerRequest; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.UserRepository; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.server.ResponseStatusException; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class CustomerServiceTest { - - @Mock - private CustomerRepository customerRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private PasswordEncoder passwordEncoder; - - @Mock - private UserBusinessLinkageService userBusinessLinkageService; - - @InjectMocks - private CustomerService customerService; - - @Test - void createCustomerCreatesLinkedUser() { - CustomerRequest request = new CustomerRequest(); - request.setFirstName("Pat"); - request.setLastName("Owner"); - request.setEmail("pat@example.com"); - - Customer savedCustomer = new Customer(); - savedCustomer.setCustomerId(7L); - savedCustomer.setFirstName("Pat"); - savedCustomer.setLastName("Owner"); - savedCustomer.setEmail("pat@example.com"); - - when(customerRepository.save(any(Customer.class))).thenReturn(savedCustomer); - when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.empty()); - when(passwordEncoder.encode(any())).thenReturn("hashed-temp-password"); - when(userRepository.save(any(User.class))).thenAnswer(invocation -> { - User user = invocation.getArgument(0); - user.setId(11L); - return user; - }); - when(userBusinessLinkageService.ensureLinkedCustomer(any(User.class))).thenAnswer(invocation -> { - User user = invocation.getArgument(0); - savedCustomer.setUserId(user.getId()); - return savedCustomer; - }); - - var response = customerService.createCustomer(request); - - assertNotNull(response); - assertEquals("Pat", response.getFirstName()); - assertEquals("Owner", response.getLastName()); - assertEquals("pat@example.com", response.getEmail()); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(userRepository).save(userCaptor.capture()); - User createdUser = userCaptor.getValue(); - assertEquals("customer_7", createdUser.getUsername()); - assertEquals("hashed-temp-password", createdUser.getPassword()); - assertEquals("pat@example.com", createdUser.getEmail()); - assertEquals("Pat Owner", createdUser.getFullName()); - assertEquals("200-000-0007", createdUser.getPhone()); - assertEquals(false, createdUser.getActive()); - } - - @Test - void createCustomerRejectsExistingNonCustomerEmail() { - CustomerRequest request = new CustomerRequest(); - request.setFirstName("Pat"); - request.setLastName("Owner"); - request.setEmail("pat@example.com"); - - User existing = new User(); - existing.setId(22L); - existing.setUsername("staff1"); - existing.setEmail("pat@example.com"); - existing.setRole(User.Role.STAFF); - - when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); - - assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); - } - - @Test - void createCustomerRejectsExistingCustomerEmail() { - CustomerRequest request = new CustomerRequest(); - request.setFirstName("Pat"); - request.setLastName("Owner"); - request.setEmail("pat@example.com"); - - User existing = new User(); - existing.setId(22L); - existing.setUsername("customer1"); - existing.setEmail("pat@example.com"); - existing.setRole(User.Role.CUSTOMER); - - when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); - - assertThrows(ResponseStatusException.class, () -> customerService.createCustomer(request)); - } - - @Test - void updateCustomerRejectsExistingEmailFromOtherUser() { - Customer customer = new Customer(); - customer.setCustomerId(7L); - customer.setUserId(11L); - customer.setFirstName("Pat"); - customer.setLastName("Owner"); - customer.setEmail("old@example.com"); - - CustomerRequest request = new CustomerRequest(); - request.setFirstName("Pat"); - request.setLastName("Owner"); - request.setEmail("pat@example.com"); - - User existing = new User(); - existing.setId(22L); - existing.setUsername("customer2"); - existing.setEmail("pat@example.com"); - existing.setRole(User.Role.CUSTOMER); - - when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); - when(userRepository.findByEmail("pat@example.com")).thenReturn(Optional.of(existing)); - - assertThrows(ResponseStatusException.class, () -> customerService.updateCustomer(7L, request)); - } - - @Test - void deleteCustomerDeletesLinkedUser() { - Customer customer = new Customer(); - customer.setCustomerId(7L); - customer.setUserId(11L); - - when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); - when(userRepository.existsById(11L)).thenReturn(true); - - customerService.deleteCustomer(7L); - - verify(userRepository).deleteById(11L); - verify(customerRepository, never()).deleteById(7L); - } - - @Test - void deleteCustomerDeletesCustomerWhenNoLinkedUserExists() { - Customer customer = new Customer(); - customer.setCustomerId(7L); - customer.setUserId(11L); - - when(customerRepository.findById(7L)).thenReturn(Optional.of(customer)); - when(userRepository.existsById(11L)).thenReturn(false); - - customerService.deleteCustomer(7L); - - verify(customerRepository).deleteById(7L); - } -} diff --git a/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java b/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java deleted file mode 100644 index b0ffeded..00000000 --- a/backend/src/test/java/com/petshop/backend/service/SaleServiceTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.petshop.backend.service; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class SaleServiceTest { - - @Test - void normalizePaymentMethodMapsDebitToCard() { - SaleService saleService = new SaleService(null, null, null, null, null, null, null, null); - - assertEquals("Card", saleService.normalizePaymentMethod("Debit")); - assertEquals("Card", saleService.normalizePaymentMethod("debit")); - assertEquals("Cash", saleService.normalizePaymentMethod("Cash")); - } -} From e1f6d8cae25cf898329821459117d164e1fb96da Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 1 Apr 2026 16:57:27 -0600 Subject: [PATCH 030/137] Apply service logic --- .gitignore | 1 + .../backend/service/CustomerService.java | 66 +++++++++++++++++-- .../petshop/backend/service/SaleService.java | 20 +++++- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index c4c4ffc6..733370d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.zip +.local/ diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java index 040be22a..33c731d1 100644 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ b/backend/src/main/java/com/petshop/backend/service/CustomerService.java @@ -4,23 +4,34 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.customer.CustomerRequest; import com.petshop.backend.dto.customer.CustomerResponse; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import static org.springframework.http.HttpStatus.CONFLICT; @Service public class CustomerService { + private static final String TEMP_PASSWORD = "TempPass123!"; + private final CustomerRepository customerRepository; private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserBusinessLinkageService userBusinessLinkageService; - public CustomerService(CustomerRepository customerRepository, UserRepository userRepository) { + public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { this.customerRepository = customerRepository; this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userBusinessLinkageService = userBusinessLinkageService; } public Page getAllCustomers(String query, Pageable pageable) { @@ -41,14 +52,19 @@ public class CustomerService { @Transactional public CustomerResponse createCustomer(CustomerRequest request) { + ensureEmailAvailable(request.getEmail(), null); + Customer customer = new Customer(); customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); customer = customerRepository.save(customer); - syncLinkedUser(customer); - return mapToResponse(customer); + User user = createLinkedUser(customer); + + Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); + syncLinkedUser(linkedCustomer); + return mapToResponse(linkedCustomer); } @Transactional @@ -56,6 +72,8 @@ public class CustomerService { Customer customer = customerRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + ensureEmailAvailable(request.getEmail(), customer.getUserId()); + customer.setFirstName(request.getFirstName()); customer.setLastName(request.getLastName()); customer.setEmail(request.getEmail()); @@ -67,9 +85,14 @@ public class CustomerService { @Transactional public void deleteCustomer(Long id) { - if (!customerRepository.existsById(id)) { - throw new ResourceNotFoundException("Customer not found with id: " + id); + Customer customer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); + + if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) { + userRepository.deleteById(customer.getUserId()); + return; } + customerRepository.deleteById(id); } @@ -99,4 +122,37 @@ public class CustomerService { userRepository.save(user); }); } + + private User createLinkedUser(Customer customer) { + User user = new User(); + user.setUsername(generateUsername(customer)); + user.setPassword(passwordEncoder.encode(TEMP_PASSWORD)); + user.setEmail(customer.getEmail()); + user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); + user.setPhone(generatePhone(customer)); + user.setRole(User.Role.CUSTOMER); + user.setActive(false); + user.setTokenVersion(0); + return userRepository.save(user); + } + + private String generateUsername(Customer customer) { + return "customer_" + customer.getCustomerId(); + } + + private String generatePhone(Customer customer) { + return String.format("200-000-%04d", customer.getCustomerId()); + } + + private void ensureEmailAvailable(String email, Long currentUserId) { + if (email == null || email.isBlank()) { + return; + } + + userRepository.findByEmail(email).ifPresent(existing -> { + if (currentUserId == null || !existing.getId().equals(currentUserId)) { + throw new ResponseStatusException(CONFLICT, "Email already exists"); + } + }); + } } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b426dc38..b8d5861e 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -78,7 +78,7 @@ public class SaleService { sale.setSaleDate(LocalDateTime.now()); sale.setEmployee(employee); sale.setStore(store); - sale.setPaymentMethod(request.getPaymentMethod()); + sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); if (request.getCustomerId() != null) { @@ -215,4 +215,22 @@ public class SaleService { return response; } + + String normalizePaymentMethod(String paymentMethod) { + if (paymentMethod == null) { + return null; + } + + String normalized = paymentMethod.trim(); + if (normalized.equalsIgnoreCase("Debit")) { + return "Card"; + } + if (normalized.equalsIgnoreCase("Cash")) { + return "Cash"; + } + if (normalized.equalsIgnoreCase("Card")) { + return "Card"; + } + return normalized; + } } From 2ac2ce339f5f6941f1a9acdbdc392f94ca258918 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 1 Apr 2026 18:08:37 -0600 Subject: [PATCH 031/137] Fix phone formatting --- .../petshop/backend/entity/StoreLocation.java | 3 +- .../com/petshop/backend/entity/Supplier.java | 3 +- .../java/com/petshop/backend/entity/User.java | 3 +- .../com/petshop/backend/util/PhoneUtils.java | 24 +++++++++++++ .../V14__normalize_phone_numbers.sql | 34 +++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/util/PhoneUtils.java create mode 100644 backend/src/main/resources/db/migration/V14__normalize_phone_numbers.sql diff --git a/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java index 6b1a2ced..8c6a9c76 100644 --- a/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java +++ b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java @@ -1,5 +1,6 @@ package com.petshop.backend.entity; +import com.petshop.backend.util.PhoneUtils; import jakarta.persistence.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -77,7 +78,7 @@ public class StoreLocation { } public void setPhone(String phone) { - this.phone = phone; + this.phone = PhoneUtils.normalize(phone); } public String getEmail() { diff --git a/backend/src/main/java/com/petshop/backend/entity/Supplier.java b/backend/src/main/java/com/petshop/backend/entity/Supplier.java index 5dc35f17..911aad08 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Supplier.java +++ b/backend/src/main/java/com/petshop/backend/entity/Supplier.java @@ -1,5 +1,6 @@ package com.petshop.backend.entity; +import com.petshop.backend.util.PhoneUtils; import jakarta.persistence.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -97,7 +98,7 @@ public class Supplier { } public void setSupPhone(String supPhone) { - this.supPhone = supPhone; + this.supPhone = PhoneUtils.normalize(supPhone); } public LocalDateTime getCreatedAt() { diff --git a/backend/src/main/java/com/petshop/backend/entity/User.java b/backend/src/main/java/com/petshop/backend/entity/User.java index cdec2754..d13e2e68 100644 --- a/backend/src/main/java/com/petshop/backend/entity/User.java +++ b/backend/src/main/java/com/petshop/backend/entity/User.java @@ -1,5 +1,6 @@ package com.petshop.backend.entity; +import com.petshop.backend.util.PhoneUtils; import jakarta.persistence.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -118,7 +119,7 @@ public class User { } public void setPhone(String phone) { - this.phone = phone; + this.phone = PhoneUtils.normalize(phone); } public String getAvatarUrl() { diff --git a/backend/src/main/java/com/petshop/backend/util/PhoneUtils.java b/backend/src/main/java/com/petshop/backend/util/PhoneUtils.java new file mode 100644 index 00000000..cf9646ba --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/PhoneUtils.java @@ -0,0 +1,24 @@ +package com.petshop.backend.util; + +public class PhoneUtils { + public static String normalize(String phone) { + if (phone == null) { + return null; + } + + String digits = phone.replaceAll("\\D", ""); + + if (digits.length() >= 10) { + if (digits.length() == 11 && digits.startsWith("1")) { + digits = digits.substring(1); + } + digits = digits.substring(0, 10); + return String.format("(%s) %s-%s", + digits.substring(0, 3), + digits.substring(3, 6), + digits.substring(6)); + } + + return phone; + } +} diff --git a/backend/src/main/resources/db/migration/V14__normalize_phone_numbers.sql b/backend/src/main/resources/db/migration/V14__normalize_phone_numbers.sql new file mode 100644 index 00000000..80da343c --- /dev/null +++ b/backend/src/main/resources/db/migration/V14__normalize_phone_numbers.sql @@ -0,0 +1,34 @@ +-- Normalize existing phone numbers to (XXX) XXX-XXXX format + +-- Update users table +UPDATE users +SET phone = '(' || SUBSTRING(clean_digits, 1, 3) || ') ' || SUBSTRING(clean_digits, 4, 3) || '-' || SUBSTRING(clean_digits, 7, 4) +FROM ( + SELECT id, + RIGHT(regexp_replace(phone, '\D', '', 'g'), 10) as clean_digits + FROM users + WHERE regexp_replace(phone, '\D', '', 'g') ~ '\d{10,}$' +) AS sub +WHERE users.id = sub.id; + +-- Update supplier table +UPDATE supplier +SET supPhone = '(' || SUBSTRING(clean_digits, 1, 3) || ') ' || SUBSTRING(clean_digits, 4, 3) || '-' || SUBSTRING(clean_digits, 7, 4) +FROM ( + SELECT supId, + RIGHT(regexp_replace(supPhone, '\D', '', 'g'), 10) as clean_digits + FROM supplier + WHERE regexp_replace(supPhone, '\D', '', 'g') ~ '\d{10,}$' +) AS sub +WHERE supplier.supId = sub.supId; + +-- Update storeLocation table +UPDATE storeLocation +SET phone = '(' || SUBSTRING(clean_digits, 1, 3) || ') ' || SUBSTRING(clean_digits, 4, 3) || '-' || SUBSTRING(clean_digits, 7, 4) +FROM ( + SELECT storeId, + RIGHT(regexp_replace(phone, '\D', '', 'g'), 10) as clean_digits + FROM storeLocation + WHERE regexp_replace(phone, '\D', '', 'g') ~ '\d{10,}$' +) AS sub +WHERE storeLocation.storeId = sub.storeId; From 14e3b89baf00633387bc3935e460860b05d4302c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 1 Apr 2026 18:10:20 -0600 Subject: [PATCH 032/137] Add desktop icons --- .../petshopdesktop/main-layout-view.fxml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml index 4e1cee6f..bbd78692 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -91,7 +91,7 @@ - - - - - - - - - - - - @@ -125,25 +120,6 @@ export default function AdoptPage() {

)} - {!loading && totalPages > 1 && ( -
- - Page {page + 1} of {totalPages} - -
- )}
); diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index d3f7943e..bec8bd78 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -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) => ( {d} ))} - {cells.map((day, i) => + {cells.map(({ key, day }) => day === null ? ( - + ) : ( + ))} + + )} + + )} + + {serviceId && ( +
+ {petSectionLabel} + {petsToShow.length === 0 ? ( +

{noPetsMessage}

+ ) : isAdoptionService ? ( +
+ {petsToShow.map((p) => ( + + ))} +
+ ) : ( +
+ {petsToShow.map((p) => ( + + ))} +
+ )} +
+ )} + + + + ) : ( +
+

Appointment Booking

-

{selectedService.serviceDesc}

+

Web appointment booking is currently available for customer accounts only.

+

Admin and staff accounts can still review appointment activity below.

- )} - -
- Date -
- - {storeId && serviceId && appointmentDate && ( -
- Available Time Slots - {loadingSlots ? ( -

Checking availability...

- ) : availableSlots.length === 0 ? ( -

No available slots for this date. Please try another date.

- ) : ( -
- {availableSlots.map((slot) => ( - - ))} -
- )} -
- )} - - {serviceId && ( -
- {petSectionLabel} - {petsToShow.length === 0 ? ( -

{noPetsMessage}

- ) : isAdoptionService ? ( -
- {petsToShow.map((p) => ( - - ))} -
- ) : ( -
- {petsToShow.map((p) => ( - - ))} -
- )} -
- )} - - - + )}
-

Your Appointments

+

{canBookAppointments ? "Your Appointments" : "Appointments"}

{loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? ( @@ -637,4 +662,8 @@ export default function AppointmentsPage() { ); -} \ No newline at end of file +} + +export default dynamic(() => Promise.resolve(AppointmentsPage), { + ssr: false, +}); diff --git a/web/app/contact/page.js b/web/app/contact/page.js new file mode 100644 index 00000000..85fba175 --- /dev/null +++ b/web/app/contact/page.js @@ -0,0 +1,73 @@ +const LOCATIONS = [ + { + name: "Downtown Branch", + address: "123 Main St", + phone: "(123) 456-7890", + email: "downtown@petshop.com", + }, + { + name: "North Branch", + address: "456 North Ave", + phone: "(987) 654-3210", + email: "north@petshop.com", + }, + { + name: "West Side Store", + address: "789 West Blvd", + phone: "(555) 123-4567", + email: "westside@petshop.com", + }, +]; + +const PERSONNEL = [ + { name: "John Doe", role: "Store Manager" }, + { name: "Sara Smith", role: "Staff" }, + { name: "Michael Johnson", role: "Grooming Team" }, +]; + +export default function ContactPage() { + return ( +
+
+

Contact Us

+

Reach the team, find a location, or connect with store personnel.

+
+
+ +
+
+

General Contact

+

Email: support@petshop.com

+

Phone: (000) 000-0000

+

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

+
+ +
+

Store Locations

+
+ {LOCATIONS.map((location) => ( +
+

{location.name}

+

{location.address}

+

{location.phone}

+

{location.email}

+
+ ))} +
+
+ +
+

Store Personnel

+
+ {PERSONNEL.map((person) => ( +
+

{person.name}

+

{person.role}

+
+ ))} +
+
+
+
+ ); +} diff --git a/web/app/globals.css b/web/app/globals.css index 500ccbb0..da8ce03b 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -617,6 +617,74 @@ body { min-height: 100vh; } +.info-page { + min-height: 100vh; + background: linear-gradient(to bottom, #f9f9f9, #ffffff); +} + +.info-hero { + text-align: center; + padding: 4rem 2rem 3rem; +} + +.info-title { + font-size: 3rem; + color: #333; + margin-bottom: 1rem; + font-weight: 700; +} + +.info-subtitle { + font-size: 1.25rem; + color: #666; + margin-bottom: 1.5rem; +} + +.info-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem 4rem; + display: grid; + gap: 1.5rem; +} + +.info-card { + background: white; + border-radius: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + padding: 1.5rem; +} + +.info-card h2 { + margin-top: 0; + margin-bottom: 1rem; + color: #222; +} + +.info-list { + margin: 0; + padding-left: 1.2rem; + display: grid; + gap: 0.5rem; +} + +.info-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +.info-mini-card { + background: #fff8ee; + border-radius: 12px; + padding: 1rem; +} + +.info-mini-card h3 { + margin-top: 0; + margin-bottom: 0.5rem; +} + .products-hero { text-align: center; padding: 4rem 2rem 3rem; @@ -1698,4 +1766,4 @@ body { .profile-pet-cancel-btn:hover { border-color: #999; color: #333; -} \ No newline at end of file +} diff --git a/web/app/login/page.js b/web/app/login/page.js index 32c05c52..dcc78d02 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -64,10 +65,10 @@ export default function LoginPage() { {loading ? "Logging in…" : "Log In"} - +

Don't have an account?{" "} - Register here + Register here

diff --git a/web/app/page.js b/web/app/page.js index 4819bd7d..91f71946 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -4,15 +4,21 @@ import Image from "next/image"; import Link from "next/link"; import { useState, useEffect } from "react"; -export default function Home() { - //Slideshow images array - const slideshowImages = [ - {src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets"}, - {src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies"}, - {src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming"}, - {src: "/images/home/slideshow/pet4.jpg", alt: "Pet food"}, - ]; +const slideshowImages = [ + {id: "slide-1", src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets"}, + {id: "slide-2", src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies"}, + {id: "slide-3", src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming"}, + {id: "slide-4", src: "/images/home/slideshow/pet4.jpg", alt: "Pet food"}, +]; +const navImages = [ + {id: "nav-adopt", src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet"}, + {id: "nav-products", src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/products", title: "Online Store"}, + {id: "nav-appointments", src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments"}, + {id: "nav-about", src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us"}, +]; + +export default function Home() { const [currentSlide, setCurrentSlide] = useState(0); //Auto-advance slideshow @@ -21,15 +27,7 @@ export default function Home() { const timer = setInterval(() => {setCurrentSlide((prev) => (prev + 1) % slideshowImages.length);}, 7500); return () => clearInterval(timer); - }, [slideshowImages.length]); - - //Hyperlinks to other pages - const navImages = [ - {src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet"}, - {src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store"}, - {src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments"}, - {src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us"}, - ]; + }, []); return (
@@ -37,7 +35,7 @@ export default function Home() {
{slideshowImages.map((image, index) => (
{navImages.map((item, index) => ( - +
); -} \ No newline at end of file +} diff --git a/web/app/products/[id]/page.js b/web/app/products/[id]/page.js index 833cbd24..47a3059b 100644 --- a/web/app/products/[id]/page.js +++ b/web/app/products/[id]/page.js @@ -17,9 +17,6 @@ export default function ProductDetailPage() { if (!id) { return; } - - setLoading(true); - setError(null); fetch(`${API_BASE}/api/v1/products/${id}`) .then((res) => { diff --git a/web/app/products/page.js b/web/app/products/page.js index f44475f1..b3416326 100644 --- a/web/app/products/page.js +++ b/web/app/products/page.js @@ -11,16 +11,11 @@ export default function ProductsPage() { const [error, setError] = useState(null); const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); - const [page, setPage] = useState(0); - const [totalPages, setTotalPages] = useState(0); - const PAGE_SIZE = 12; + const PAGE_SIZE = 200; useEffect(() => { - setLoading(true); - setError(null); - - const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "prodId,asc" }); + const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "prodId,asc" }); if (query) { params.set("q", query); } @@ -35,15 +30,15 @@ export default function ProductsPage() { }) .then((data) => { setProducts(data.content ?? []); - setTotalPages(data.totalPages ?? 0); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); - }, [page, query]); + }, [query]); function handleSearch(e) { e.preventDefault(); - setPage(0); + setLoading(true); + setError(null); setQuery(search.trim()); } @@ -70,7 +65,7 @@ export default function ProductsPage() { @@ -108,25 +103,6 @@ export default function ProductsPage() {
)} - {!loading && totalPages > 1 && ( -
- - Page {page + 1} of {totalPages} - -
- )} ); diff --git a/web/app/register/page.js b/web/app/register/page.js index 20dcd2c1..eb9de480 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -140,10 +141,10 @@ export default function RegisterPage() { {loading ? "Creating account…" : "Register"} - +

Already have an account?{" "} - Log in here + Log in here

diff --git a/web/components/Navigation.js b/web/components/Navigation.js index cfd17924..3259c1d1 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -1,6 +1,7 @@ "use client"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -23,31 +24,31 @@ export default function DisplayNav() { id="logo"/>
- Home - Adopt a Pet - Online Store - Schedule an Appointment - Contact Us - About Us + Home + Adopt a Pet + Online Store + Schedule an Appointment + Contact Us + About Us
{loading ? null : user ? ( <> - + Hello, {user.fullName || user.username} - - ) : ( <> - Log In - Register + Log In + Register )}
); -} \ No newline at end of file +} From a76895434d28d7b4ea233f3b68dbd40da536319d Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:06:24 -0600 Subject: [PATCH 039/137] Improve auth flows --- web/app/appointments/page.js | 5 +- web/app/globals.css | 38 +++++++ web/app/login/page.js | 24 ++++- web/app/profile/page.js | 194 +++++++++++++++++++++++++++++++++-- web/app/register/page.js | 24 ++++- web/context/AuthContext.js | 54 ++++++---- 6 files changed, 300 insertions(+), 39 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index bec8bd78..fac5a86f 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -226,10 +226,11 @@ function AppointmentsPage() { useEffect(() => { if (!authLoading && !user) { - router.push("/login"); + const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments"; + router.push(`/login?next=${encodeURIComponent(target)}`); } - }, [authLoading, user, router]); + }, [authLoading, user, router, preselectedPetId]); useEffect(() => { if (!token) { diff --git a/web/app/globals.css b/web/app/globals.css index da8ce03b..8571bfeb 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1076,6 +1076,13 @@ body { align-items: center; justify-content: center; margin-bottom: 0.25rem; + overflow: hidden; +} + +.profile-avatar-image { + width: 100%; + height: 100%; + object-fit: cover; } .profile-name { @@ -1105,6 +1112,37 @@ body { padding-top: 1rem; } +.profile-update-form { + width: 100%; + display: grid; + gap: 0.9rem; +} + +.profile-update-title { + margin: 0.25rem 0 0; + font-size: 1.1rem; + color: #222; +} + +.profile-avatar-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.profile-avatar-upload-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.65rem 1rem; + border-radius: 8px; + background: #fff3e0; + color: #a65c00; + font-weight: 600; + cursor: pointer; +} + .profile-field-row { display: flex; justify-content: space-between; diff --git a/web/app/login/page.js b/web/app/login/page.js index dcc78d02..2e7d1fe1 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -1,13 +1,25 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -export default function LoginPage() { +function resolveNextPath(candidate) { + if (!candidate || !candidate.startsWith("/")) { + return "/"; + } + if (candidate.startsWith("//") || candidate.startsWith("/login") || candidate.startsWith("/register")) { + return "/"; + } + return candidate; +} + +function LoginPage() { const {login} = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -21,7 +33,7 @@ export default function LoginPage() { try { await login(username, password); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -68,9 +80,13 @@ export default function LoginPage() {

Don't have an account?{" "} - Register here + Register here

); } + +export default dynamic(() => Promise.resolve(LoginPage), { + ssr: false, +}); diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 6b28aa35..d0b78c5e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -7,7 +7,7 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; export default function ProfilePage() { - const {user, token, loading, logout} = useAuth(); + const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); const [pets, setPets] = useState([]); @@ -19,14 +19,27 @@ export default function ProfilePage() { const [breed, setBreed] = useState(""); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); + const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); + const [profileSubmitting, setProfileSubmitting] = useState(false); + const [profileError, setProfileError] = useState(null); + const [profileSuccess, setProfileSuccess] = useState(null); + const [avatarSubmitting, setAvatarSubmitting] = useState(false); useEffect(() => { if (!loading && !user) { - router.replace("/login"); + router.replace(`/login?next=${encodeURIComponent("/profile")}`); } }, [user, loading, router]); + useEffect(() => { + setProfileForm({ + fullName: user?.fullName || "", + email: user?.email || "", + phone: user?.phone || "", + }); + }, [user]); + const loadPets = useCallback(() => { if (!token) return; setLoadingPets(true); @@ -50,6 +63,105 @@ export default function ProfilePage() { router.push("/"); } + async function handleProfileSubmit(e) { + e.preventDefault(); + setProfileSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(profileForm), + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || `Request failed (${res.status})`); + } + + await refreshUser(); + setProfileSuccess("Profile updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setProfileSubmitting(false); + } + } + + async function handleAvatarUpload(file) { + if (!file) { + return; + } + + const formData = new FormData(); + formData.append("avatar", file); + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to upload avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + + async function handleAvatarDelete() { + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to delete avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar removed successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + function openAddForm() { setEditingPet(null); setPetName(""); @@ -170,7 +282,11 @@ export default function ProfilePage() {
- {(user.fullName || user.username).charAt(0).toUpperCase()} + {user.avatarUrl ? ( + {user.fullName + ) : ( + (user.fullName || user.username).charAt(0).toUpperCase() + )}

{user.fullName || user.username}

@@ -185,7 +301,65 @@ export default function ProfilePage() { ))} - + )} +
+ + + + @@ -194,7 +368,7 @@ export default function ProfilePage() {

My Pets

- +
{showForm && ( @@ -285,11 +459,11 @@ export default function ProfilePage() { {pet.breed && {pet.breed}}
- - -
- - ))} + + + + + ))} )} diff --git a/web/app/register/page.js b/web/app/register/page.js index eb9de480..17f49672 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -1,13 +1,25 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -export default function RegisterPage() { +function resolveNextPath(candidate) { + if (!candidate || !candidate.startsWith("/")) { + return "/"; + } + if (candidate.startsWith("//") || candidate.startsWith("/login") || candidate.startsWith("/register")) { + return "/"; + } + return candidate; +} + +function RegisterPage() { const {register} = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); const [form, setForm] = useState({ fullName: "", @@ -41,7 +53,7 @@ export default function RegisterPage() { phone: form.phone, password: form.password, }); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -144,9 +156,13 @@ export default function RegisterPage() {

Already have an account?{" "} - Log in here + Log in here

); } + +export default dynamic(() => Promise.resolve(RegisterPage), { + ssr: false, +}); diff --git a/web/context/AuthContext.js b/web/context/AuthContext.js index 5a9b0d08..e861f624 100644 --- a/web/context/AuthContext.js +++ b/web/context/AuthContext.js @@ -19,6 +19,28 @@ export function AuthProvider({ children }) { const [token, setToken] = useState(null); const [loading, setLoading] = useState(true); + const refreshUser = useCallback(async (providedToken) => { + const activeToken = providedToken ?? token; + if (!activeToken) { + setUser(null); + return null; + } + + const userInfo = await fetchCurrentUser(activeToken); + if (!userInfo) { + localStorage.removeItem(TOKEN_KEY); + setToken(null); + setUser(null); + return null; + } + + if (!token) { + setToken(activeToken); + } + setUser(userInfo); + return userInfo; + }, [token]); + useEffect(() => { const stored = localStorage.getItem(TOKEN_KEY); if (!stored) { @@ -26,17 +48,14 @@ export function AuthProvider({ children }) { return; } - fetchCurrentUser(stored) - .then((data) => { - if (data) { - setToken(stored); - setUser(data); - } - - else { - localStorage.removeItem(TOKEN_KEY); - } - }).catch(() => localStorage.removeItem(TOKEN_KEY)).finally(() => setLoading(false));}, []); + refreshUser(stored) + .catch(() => { + localStorage.removeItem(TOKEN_KEY); + setToken(null); + setUser(null); + }) + .finally(() => setLoading(false)); + }, [refreshUser]); const login = useCallback(async (username, password) => { const res = await fetch("/api/v1/auth/login", { @@ -55,12 +74,10 @@ export function AuthProvider({ children }) { localStorage.setItem(TOKEN_KEY, jwt); setToken(jwt); - const userInfo = await fetchCurrentUser(jwt); - - setUser(userInfo); + const userInfo = await refreshUser(jwt); return userInfo; - }, []); + }, [refreshUser]); const register = useCallback(async ({ username, password, email, fullName, phone }) => { const res = await fetch("/api/v1/auth/register", { @@ -79,11 +96,10 @@ export function AuthProvider({ children }) { localStorage.setItem(TOKEN_KEY, jwt); setToken(jwt); - const userInfo = await fetchCurrentUser(jwt); - setUser(userInfo); + const userInfo = await refreshUser(jwt); return userInfo; - }, []); + }, [refreshUser]); const logout = useCallback(() => { localStorage.removeItem(TOKEN_KEY); @@ -91,7 +107,7 @@ export function AuthProvider({ children }) { setUser(null);}, []); return ( - + {children} ); From 781eb48ca9b73bfccc10367810b27aab90eafc13 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:08:00 -0600 Subject: [PATCH 040/137] Fix item loading --- web/app/adopt/page.js | 30 +++++++++++++++++++----------- web/app/products/page.js | 32 +++++++++++++++++--------------- web/components/PetCard.js | 10 +++++++++- web/components/PetProfile.js | 10 +++++++++- web/components/ProductCard.js | 10 +++++++++- web/components/ProductProfile.js | 10 +++++++++- web/lib/fetchAllPages.js | 19 +++++++++++++++++++ 7 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 web/lib/fetchAllPages.js diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 90293d8c..97fe1472 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import PetCard from "@/components/PetCard"; +import { fetchAllPages } from "@/lib/fetchAllPages"; const API_BASE = ""; @@ -13,7 +14,7 @@ export default function AdoptPage() { const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); - const PAGE_SIZE = 200; + const PAGE_SIZE = 100; useEffect(() => { fetch(`${API_BASE}/api/v1/health`) @@ -22,16 +23,23 @@ export default function AdoptPage() { }, []); useEffect(() => { - const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "petId,asc" }); - if (query) params.set("q", query); + setLoading(true); + setError(null); - fetch(`${API_BASE}/api/v1/pets?${params}`) - .then((res) => { - if (!res.ok) throw new Error(`HTTP ${res.status} – ${res.statusText}`); - return res.json(); - }) - .then((data) => { - setPets(data.content ?? []); + fetchAllPages((page) => { + const params = new URLSearchParams({ + page: String(page), + size: String(PAGE_SIZE), + sort: "petId,asc", + status: "Available", + }); + if (query) { + params.set("q", query); + } + return `${API_BASE}/api/v1/pets?${params}`; + }) + .then((allPets) => { + setPets(allPets); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); @@ -58,7 +66,7 @@ export default function AdoptPage() { setSearch(e.target.value)} /> diff --git a/web/app/products/page.js b/web/app/products/page.js index b3416326..19cf8670 100644 --- a/web/app/products/page.js +++ b/web/app/products/page.js @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import ProductCard from "@/components/ProductCard"; +import { fetchAllPages } from "@/lib/fetchAllPages"; const API_BASE = ""; @@ -12,24 +13,25 @@ export default function ProductsPage() { const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); - const PAGE_SIZE = 200; + const PAGE_SIZE = 100; useEffect(() => { - const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "prodId,asc" }); - if (query) { - params.set("q", query); - } + setLoading(true); + setError(null); - fetch(`${API_BASE}/api/v1/products?${params}`) - .then((res) => { - if (!res.ok) { - throw new Error(`HTTP ${res.status} – ${res.statusText}`); - } - - return res.json(); - }) - .then((data) => { - setProducts(data.content ?? []); + fetchAllPages((page) => { + const params = new URLSearchParams({ + page: String(page), + size: String(PAGE_SIZE), + sort: "prodId,asc", + }); + if (query) { + params.set("q", query); + } + return `${API_BASE}/api/v1/products?${params}`; + }) + .then((allProducts) => { + setProducts(allProducts); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); diff --git a/web/components/PetCard.js b/web/components/PetCard.js index 4370bca1..4c6410b9 100644 --- a/web/components/PetCard.js +++ b/web/components/PetCard.js @@ -5,7 +5,15 @@ export default function PetCard({petId, petName, petSpecies, petStatus, imageUrl return (
- {petName} + {petName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />

{petName}

diff --git a/web/components/PetProfile.js b/web/components/PetProfile.js index a00732a4..8b7fd6f0 100644 --- a/web/components/PetProfile.js +++ b/web/components/PetProfile.js @@ -5,7 +5,15 @@ export default function PetProfile({ petId, petName, petSpecies, petBreed, petAg return (
- {petName} + {petName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />
diff --git a/web/components/ProductCard.js b/web/components/ProductCard.js index e27abbbd..0b4c76ed 100644 --- a/web/components/ProductCard.js +++ b/web/components/ProductCard.js @@ -4,7 +4,15 @@ export default function ProductCard({ prodId, prodName, categoryName, prodPrice, return (
- {prodName} + {prodName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />

{prodName}

diff --git a/web/components/ProductProfile.js b/web/components/ProductProfile.js index b8d0a9b2..fda41420 100644 --- a/web/components/ProductProfile.js +++ b/web/components/ProductProfile.js @@ -4,7 +4,15 @@ export default function ProductProfile({ prodName, categoryName, prodDesc, prodP return (
- {prodName} + {prodName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />
diff --git a/web/lib/fetchAllPages.js b/web/lib/fetchAllPages.js new file mode 100644 index 00000000..624d172c --- /dev/null +++ b/web/lib/fetchAllPages.js @@ -0,0 +1,19 @@ +export async function fetchAllPages(urlBuilder) { + const items = []; + let page = 0; + let totalPages = 1; + + while (page < totalPages) { + const res = await fetch(urlBuilder(page)); + if (!res.ok) { + throw new Error(`HTTP ${res.status} – ${res.statusText}`); + } + + const data = await res.json(); + items.push(...(data.content ?? [])); + totalPages = Math.max(data.totalPages ?? 1, 1); + page += 1; + } + + return items; +} From 99855a6e99907ef827aa4385ee580644c01ccdec Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:11:53 -0600 Subject: [PATCH 041/137] Fix pet sorting --- web/app/adopt/page.js | 2 +- web/app/appointments/page.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 97fe1472..a6a2784a 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -30,7 +30,7 @@ export default function AdoptPage() { const params = new URLSearchParams({ page: String(page), size: String(PAGE_SIZE), - sort: "petId,asc", + sort: "id,asc", status: "Available", }); if (query) { diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index fac5a86f..676b953e 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -249,7 +249,7 @@ function AppointmentsPage() { .then((data) => setServices(data.content ?? [])) .catch(() => {}); - fetch(`${API_BASE}/api/v1/pets?size=200&sort=petId,asc`) + fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc&status=Available`) .then((r) => r.json()) .then((data) => setAllPets(data.content ?? [])) .catch(() => {}); From 63162487b540aaa08b312171d912b1192f2c9639 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:32:42 -0600 Subject: [PATCH 042/137] Fix profile images --- web/app/appointments/page.js | 10 +---- web/app/profile/page.js | 75 +++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 676b953e..4643bf0e 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -615,15 +615,7 @@ function AppointmentsPage() { {submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"} - ) : ( -
-

Appointment Booking

-
-

Web appointment booking is currently available for customer accounts only.

-

Admin and staff accounts can still review appointment activity below.

-
-
- )} + ) : null}

{canBookAppointments ? "Your Appointments" : "Appointments"}

diff --git a/web/app/profile/page.js b/web/app/profile/page.js index d0b78c5e..ff79521e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -9,6 +9,7 @@ const API_BASE = ""; export default function ProfilePage() { const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); + const petImageObjectUrlsRef = useRef([]); const [pets, setPets] = useState([]); const [loadingPets, setLoadingPets] = useState(false); @@ -25,6 +26,13 @@ export default function ProfilePage() { const [profileSuccess, setProfileSuccess] = useState(null); const [avatarSubmitting, setAvatarSubmitting] = useState(false); + const clearPetImageObjectUrls = useCallback(() => { + for (const objectUrl of petImageObjectUrlsRef.current) { + URL.revokeObjectURL(objectUrl); + } + petImageObjectUrlsRef.current = []; + }, []); + useEffect(() => { if (!loading && !user) { router.replace(`/login?next=${encodeURIComponent("/profile")}`); @@ -40,17 +48,64 @@ export default function ProfilePage() { }); }, [user]); - const loadPets = useCallback(() => { + const loadPets = useCallback(async () => { if (!token) return; setLoadingPets(true); - fetch(`${API_BASE}/api/v1/my-pets`, { - headers: { Authorization: `Bearer ${token}` }, - }) - .then((r) => r.json()) - .then(setPets) - .catch(() => {}) - .finally(() => setLoadingPets(false)); - }, [token]); + + try { + const response = await fetch(`${API_BASE}/api/v1/my-pets`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`Request failed (${response.status})`); + } + + const petData = await response.json(); + clearPetImageObjectUrls(); + + const petsWithResolvedImages = await Promise.all( + (Array.isArray(petData) ? petData : []).map(async (pet) => { + if (!pet.imageUrl) { + return pet; + } + + try { + const imageResponse = await fetch(`${API_BASE}${pet.imageUrl}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!imageResponse.ok) { + return { ...pet, imageUrl: null }; + } + + const blob = await imageResponse.blob(); + const objectUrl = URL.createObjectURL(blob); + petImageObjectUrlsRef.current.push(objectUrl); + + return { ...pet, imageUrl: objectUrl }; + } catch { + return { ...pet, imageUrl: null }; + } + }) + ); + + setPets(petsWithResolvedImages); + } + + catch { + } + + finally { + setLoadingPets(false); + } + }, [token, clearPetImageObjectUrls]); + + useEffect(() => { + return () => { + clearPetImageObjectUrls(); + }; + }, [clearPetImageObjectUrls]); useEffect(() => { if (user?.role === "CUSTOMER") { From 5fa9cfd5d6328cc4bf5949aa288aeebb81e526e8 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:37:43 -0600 Subject: [PATCH 043/137] added calendar view to appointments - NOTE: may have to change appointments abit after backend is updated --- android/app/build.gradle.kts | 2 + .../listfragments/AppointmentFragment.java | 113 +++++++++++++++--- .../petstoremobile/utils/EventDecorator.java | 30 +++++ .../main/res/layout/fragment_appointment.xml | 22 +++- android/gradle.properties | 2 + 5 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e3955e2f..a6d27404 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -82,6 +82,8 @@ dependencies { implementation("com.github.bumptech.glide:glide:4.16.0") annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") + implementation("com.github.prolificinteractive:material-calendarview:2.0.1") + testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index f8aa734f..49da714c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -1,5 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; +import android.graphics.Color; import android.os.Bundle; import androidx.annotation.NonNull; @@ -29,10 +30,21 @@ import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; +import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.prolificinteractive.materialcalendarview.CalendarDay; +import com.prolificinteractive.materialcalendarview.CalendarMode; +import com.prolificinteractive.materialcalendarview.MaterialCalendarView; +import com.prolificinteractive.materialcalendarview.OnDateSelectedListener; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import retrofit2.Call; import retrofit2.Callback; @@ -50,6 +62,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; private ImageButton hamburger; + private ImageButton btnToggleCalendarMode; + private MaterialCalendarView calendarView; + private CalendarDay selectedCalendarDay; + private boolean isMonthMode = false; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -58,10 +75,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. api = RetrofitClient.getAppointmentApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburger); + calendarView = view.findViewById(R.id.calendarView); + btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode); setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + setupCalendar(); loadAppointmentData(); loadPets(); loadServices(); @@ -76,9 +96,60 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. listFragment.openDrawer(); }); + btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); + return view; } + // Toggle Calendar Mode from week to month and other way around + private void toggleCalendarMode() { + isMonthMode = !isMonthMode; + calendarView.state().edit() + .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) + .commit(); + } + + private void setupCalendar() { + calendarView.setOnDateChangedListener(new OnDateSelectedListener() { + @Override + public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) { + if (selected) { + if (date.equals(selectedCalendarDay)) { + selectedCalendarDay = null; + calendarView.clearSelection(); + } else { + selectedCalendarDay = date; + } + } else { + selectedCalendarDay = null; + } + filterAppointments(etSearch.getText().toString()); + } + }); + } + + //Set indicators for dates with appointments on the calendar + private void updateCalendarDecorators() { + HashSet datesWithAppointments = new HashSet<>(); + for (AppointmentDTO appointment : appointmentList) { + try { + //Get the appointment date + Date date = dateFormat.parse(appointment.getAppointmentDate()); + //if the date is not null, add it to the hashset + if (date != null) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + datesWithAppointments.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))); + } + } catch (ParseException e) { + Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate()); + } + } + //update the indicators to the calendar + calendarView.removeDecorators(); + calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); + } + private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchAppointment); etSearch.addTextChangedListener(new TextWatcher() { @@ -99,16 +170,25 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private void filterAppointments(String query) { filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(appointmentList); - } else { - String lower = query.toLowerCase(); - for (AppointmentDTO a : appointmentList) { - if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) - || (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lower)) - || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower))) { - filteredList.add(a); - } + String lowerQuery = query.toLowerCase(); + + String selectedDateString = null; + if (selectedCalendarDay != null) { + selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", + selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); + } + + for (AppointmentDTO a : appointmentList) { + boolean matchesSearch = query.isEmpty() || + (a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || + (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lowerQuery)) || + (a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)); + + boolean matchesDate = (selectedDateString == null) || + (a.getAppointmentDate() != null && a.getAppointmentDate().equals(selectedDateString)); + + if (matchesSearch && matchesDate) { + filteredList.add(a); } } adapter.notifyDataSetChanged(); @@ -141,17 +221,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (lf != null) lf.loadFragment(detailFragment); } public void onAppointmentSaved(int position, AppointmentDTO appointment) { - if (position == -1) { - appointmentList.add(appointment); - } else { - appointmentList.set(position, appointment); - } - filterAppointments(etSearch.getText().toString()); + loadAppointmentData(); } public void onAppointmentDeleted(int position) { - appointmentList.remove(position); - filterAppointments(etSearch.getText().toString()); + loadAppointmentData(); } @Override @@ -162,7 +236,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private void loadAppointmentData() { if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); - api.getAllAppointments(0, 100).enqueue(new Callback>() { + api.getAllAppointments(0, 500).enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -171,6 +245,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (response.isSuccessful() && response.body() != null) { appointmentList.clear(); appointmentList.addAll(response.body().getContent()); + updateCalendarDecorators(); filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); } else { Log.e("AppointmentFragment", "Error: " + response.message()); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java b/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java new file mode 100644 index 00000000..b58f38d4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/EventDecorator.java @@ -0,0 +1,30 @@ +package com.example.petstoremobile.utils; + +import com.prolificinteractive.materialcalendarview.CalendarDay; +import com.prolificinteractive.materialcalendarview.DayViewDecorator; +import com.prolificinteractive.materialcalendarview.DayViewFacade; +import com.prolificinteractive.materialcalendarview.spans.DotSpan; + +import java.util.Collection; +import java.util.HashSet; + +public class EventDecorator implements DayViewDecorator { + + private final int color; + private final HashSet dates; + + public EventDecorator(int color, Collection dates) { + this.color = color; + this.dates = new HashSet<>(dates); + } + + @Override + public boolean shouldDecorate(CalendarDay day) { + return dates.contains(day); + } + + @Override + public void decorate(DayViewFacade view) { + view.addSpan(new DotSpan(8, color)); + } +} diff --git a/android/app/src/main/res/layout/fragment_appointment.xml b/android/app/src/main/res/layout/fragment_appointment.xml index a8da443e..10e17d48 100644 --- a/android/app/src/main/res/layout/fragment_appointment.xml +++ b/android/app/src/main/res/layout/fragment_appointment.xml @@ -29,15 +29,35 @@ android:contentDescription="Open menu"/> + + + + Date: Thu, 2 Apr 2026 09:54:32 -0600 Subject: [PATCH 044/137] Hide adopted pets --- .../backend/repository/PetRepository.java | 12 ++ .../petshop/backend/service/PetService.java | 71 +++++++- .../backend/service/PetServiceTest.java | 165 ++++++++++++++++++ 3 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 backend/src/test/java/com/petshop/backend/service/PetServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index a474fa8b..d974deb3 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -16,4 +16,16 @@ public interface PetRepository extends JpaRepository { "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); + + @Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species))") + Page searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable); + + @Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " + + "(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") + Page searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 4672ee85..dc5fa61e 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -7,12 +7,16 @@ import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.PetRepository; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -34,13 +38,38 @@ public class PetService { } public Page getAllPets(String query, String species, String status, Pageable pageable) { - return petRepository.searchPets(normalizeFilter(query), normalizeFilter(species), normalizeFilter(status), pageable) + String normalizedQuery = normalizeFilter(query); + String normalizedSpecies = normalizeFilter(species); + String normalizedStatus = normalizeFilter(status); + CurrentViewer viewer = getCurrentViewer(); + + Page pets; + if (viewer == null) { + if (!isAllowedPublicStatus(normalizedStatus)) { + return new PageImpl<>(java.util.List.of(), pageable, 0); + } + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + } else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) { + pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable); + } else if (viewer.role() == User.Role.CUSTOMER) { + if (!isAllowedCustomerStatus(normalizedStatus)) { + return new PageImpl<>(java.util.List.of(), pageable, 0); + } + pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable); + } else { + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + } + + return pets .map(this::mapToResponse); } public PetResponse getPetById(Long id) { Pet pet = petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); + if (!canViewPet(pet, getCurrentViewer())) { + throw new ResourceNotFoundException("Pet not found with id: " + id); + } return mapToResponse(pet); } @@ -110,7 +139,7 @@ public class PetService { if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) { throw new ResourceNotFoundException("Pet image not found for id: " + id); } - if (!canViewPetImage(pet, requesterUserId, requesterRole)) { + if (!canViewPet(pet, new CurrentViewer(requesterUserId, requesterRole))) { throw new ForbiddenImageAccessException(); } Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl()); @@ -122,14 +151,21 @@ public class PetService { return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus())); } - private boolean canViewPetImage(Pet pet, Long requesterUserId, User.Role requesterRole) { + private boolean canViewPet(Pet pet, CurrentViewer viewer) { if (isPubliclyVisible(pet)) { return true; } - if (requesterRole == User.Role.STAFF || requesterRole == User.Role.ADMIN) { + if (viewer != null && (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN)) { return true; } - if (requesterUserId == null) { + if (viewer == null || viewer.userId() == null) { + return false; + } + return isAdoptedByUser(pet, viewer.userId()); + } + + private boolean isAdoptedByUser(Pet pet, Long userId) { + if (userId == null) { return false; } if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) { @@ -137,10 +173,22 @@ public class PetService { } return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed") .map(Adoption::getCustomer) - .map(customer -> requesterUserId.equals(customer.getUserId())) + .map(customer -> userId.equals(customer.getUserId())) .orElse(false); } + private CurrentViewer getCurrentViewer() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof AppPrincipal appPrincipal) { + return new CurrentViewer(appPrincipal.getUserId(), appPrincipal.getRole()); + } + return null; + } + private Pet findPet(Long id) { return petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); @@ -177,6 +225,14 @@ public class PetService { return status == null ? "" : status.trim(); } + private boolean isAllowedPublicStatus(String status) { + return status == null || "available".equalsIgnoreCase(status); + } + + private boolean isAllowedCustomerStatus(String status) { + return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status); + } + private String normalizeFilter(String value) { if (value == null) { return null; @@ -203,6 +259,9 @@ public class PetService { public record ImagePayload(Resource resource, MediaType mediaType) { } + private record CurrentViewer(Long userId, User.Role role) { + } + public static class ForbiddenImageAccessException extends RuntimeException { } } diff --git a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java new file mode 100644 index 00000000..9107ebd9 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java @@ -0,0 +1,165 @@ +package com.petshop.backend.service; + +import com.petshop.backend.entity.Adoption; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.security.AppPrincipal; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PetServiceTest { + + @Mock + private PetRepository petRepository; + + @Mock + private AdoptionRepository adoptionRepository; + + @Mock + private CatalogImageStorageService catalogImageStorageService; + + @InjectMocks + private PetService petService; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void getAllPetsAnonymousReturnsOnlyPublicPets() { + Pageable pageable = PageRequest.of(0, 10); + Pet availablePet = pet(1L, "Buddy", "Available"); + when(petRepository.searchPublicPets(null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(1, result.getTotalElements()); + assertEquals("Buddy", result.getContent().get(0).getPetName()); + verify(petRepository).searchPublicPets(null, null, pageable); + verify(petRepository, never()).searchPets(null, null, null, pageable); + } + + @Test + void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { + Pageable pageable = PageRequest.of(0, 10); + + var result = petService.getAllPets(null, null, "Adopted", pageable); + + assertEquals(0, result.getTotalElements()); + verify(petRepository, never()).searchPublicPets(null, null, pageable); + } + + @Test + void getAllPetsCustomerReturnsVisiblePetsOnly() { + Pageable pageable = PageRequest.of(0, 10); + setAuthentication(25L, User.Role.CUSTOMER); + Pet availablePet = pet(1L, "Buddy", "Available"); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) + .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(2, result.getTotalElements()); + verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); + } + + @Test + void getAllPetsAdminReturnsAllPets() { + Pageable pageable = PageRequest.of(0, 10); + setAuthentication(99L, User.Role.ADMIN); + Pet availablePet = pet(1L, "Buddy", "Available"); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.searchPets(null, null, null, pageable)) + .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); + + var result = petService.getAllPets(null, null, null, pageable); + + assertEquals(2, result.getTotalElements()); + verify(petRepository).searchPets(null, null, null, pageable); + } + + @Test + void getPetByIdHidesAdoptedPetFromUnrelatedCustomer() { + setAuthentication(50L, User.Role.CUSTOMER); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); + when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) + .thenReturn(Optional.of(adoption(2L, 25L))); + + assertThrows(ResourceNotFoundException.class, () -> petService.getPetById(2L)); + } + + @Test + void getPetByIdAllowsOwnerToSeeAdoptedPet() { + setAuthentication(25L, User.Role.CUSTOMER); + Pet adoptedPet = pet(2L, "Luna", "Adopted"); + when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); + when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) + .thenReturn(Optional.of(adoption(2L, 25L))); + + var result = petService.getPetById(2L); + + assertEquals(2L, result.getPetId()); + } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new AppPrincipal(userId, "user", role, 0), + "n/a", + List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) + ) + ); + } + + private Pet pet(Long id, String name, String status) { + Pet pet = new Pet(); + pet.setPetId(id); + pet.setPetName(name); + pet.setPetSpecies("Cat"); + pet.setPetBreed("Mixed"); + pet.setPetAge(2); + pet.setPetStatus(status); + pet.setPetPrice(java.math.BigDecimal.TEN); + return pet; + } + + private Adoption adoption(Long petId, Long userId) { + Adoption adoption = new Adoption(); + Pet pet = new Pet(); + pet.setPetId(petId); + adoption.setPet(pet); + Customer customer = new Customer(); + customer.setCustomerId(1L); + customer.setUserId(userId); + adoption.setCustomer(customer); + adoption.setAdoptionStatus("Completed"); + return adoption; + } +} From 109f9674354a6cb6f94d5351c7e35382932a5f47 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 2 Apr 2026 10:10:04 -0600 Subject: [PATCH 045/137] Fix appointment ownership --- .../controller/DropdownController.java | 27 ++++- .../repository/CustomerPetRepository.java | 2 + .../backend/service/AppointmentService.java | 9 +- .../service/AppointmentServiceTest.java | 111 ++++++++++++++++-- .../dto/appointment/AppointmentRequest.java | 9 ++ .../dto/appointment/AppointmentResponse.java | 18 +++ .../api/endpoints/DropdownApi.java | 8 ++ .../controllers/AppointmentController.java | 9 +- .../AppointmentDialogController.java | 59 ++++++++-- 9 files changed, 226 insertions(+), 26 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index c942eae8..56e53a56 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -1,10 +1,12 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.DropdownOption; +import com.petshop.backend.entity.CustomerPet; import com.petshop.backend.repository.*; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,6 +19,7 @@ public class DropdownController { private final PetRepository petRepository; private final CustomerRepository customerRepository; + private final CustomerPetRepository customerPetRepository; private final ServiceRepository serviceRepository; private final ProductRepository productRepository; private final CategoryRepository categoryRepository; @@ -24,11 +27,13 @@ public class DropdownController { private final SupplierRepository supplierRepository; public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, - ServiceRepository serviceRepository, ProductRepository productRepository, - CategoryRepository categoryRepository, StoreRepository storeRepository, - SupplierRepository supplierRepository) { + CustomerPetRepository customerPetRepository, + ServiceRepository serviceRepository, ProductRepository productRepository, + CategoryRepository categoryRepository, StoreRepository storeRepository, + SupplierRepository supplierRepository) { this.petRepository = petRepository; this.customerRepository = customerRepository; + this.customerPetRepository = customerPetRepository; this.serviceRepository = serviceRepository; this.productRepository = productRepository; this.categoryRepository = categoryRepository; @@ -55,6 +60,16 @@ public class DropdownController { ); } + @GetMapping("/customers/{customerId}/pets") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getCustomerPets(@PathVariable Long customerId) { + return ResponseEntity.ok( + customerPetRepository.findByCustomerCustomerIdOrderByPetNameAsc(customerId).stream() + .map(this::toCustomerPetOption) + .collect(Collectors.toList()) + ); + } + @GetMapping("/services") public ResponseEntity> getServices() { return ResponseEntity.ok( @@ -123,4 +138,10 @@ public class DropdownController { .collect(Collectors.toList()) ); } + + private DropdownOption toCustomerPetOption(CustomerPet pet) { + String species = pet.getSpecies() == null || pet.getSpecies().isBlank() ? "Pet" : pet.getSpecies(); + String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " · " + pet.getBreed(); + return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")"); + } } diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java index 8d08f8b9..4fe0ef81 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java @@ -12,5 +12,7 @@ public interface CustomerPetRepository extends JpaRepository List findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId); + List findByCustomerCustomerIdOrderByPetNameAsc(Long customerId); + Optional findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 67ce4f36..c5b615d3 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -120,7 +120,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -164,7 +164,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); appointment.setCustomer(customer); appointment.setStore(store); @@ -247,11 +247,14 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds) { + private Set fetchCustomerPets(List customerPetIds, Long customerId) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); + if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { + throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); + } customerPets.add(customerPet); } diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 2a1e6eed..e978fcde 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -2,12 +2,16 @@ package com.petshop.backend.service; import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.CustomerPet; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Service; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.CustomerPetRepository; import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; @@ -29,6 +33,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -44,6 +49,9 @@ class AppointmentServiceTest { @Mock private CustomerRepository customerRepository; + @Mock + private CustomerPetRepository customerPetRepository; + @Mock private PetRepository petRepository; @@ -56,6 +64,12 @@ class AppointmentServiceTest { @Mock private UserRepository userRepository; + @Mock + private EmployeeRepository employeeRepository; + + @Mock + private EmployeeStoreRepository employeeStoreRepository; + @InjectMocks private AppointmentService appointmentService; @@ -64,6 +78,7 @@ class AppointmentServiceTest { private Service grooming; private Service nailTrim; private Pet pet; + private CustomerPet customerPet; private LocalDate date; @BeforeEach @@ -91,6 +106,11 @@ class AppointmentServiceTest { pet.setPetId(1L); pet.setPetName("Milo"); + customerPet = new CustomerPet(); + customerPet.setCustomerPetId(11L); + customerPet.setPetName("Milo Jr"); + customerPet.setCustomer(customer); + date = LocalDate.now().plusDays(1); } @@ -144,14 +164,7 @@ class AppointmentServiceTest { user.setRole(User.Role.CUSTOMER); user.setTokenVersion(0); when(userRepository.findById(10L)).thenReturn(Optional.of(user)); - - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken( - new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0), - "n/a", - List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER")) - ) - ); + setAuthentication(10L, User.Role.CUSTOMER); when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); @@ -176,6 +189,78 @@ class AppointmentServiceTest { assertEquals("Booked", response.getAppointmentStatus()); } + @Test + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(2L); + + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + otherCustomerPet.setPetName("Not Yours"); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(22L)); + + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + } + + @Test + void createAppointmentAllowsCustomerOwnedCustomerPet() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appointment = invocation.getArgument(0); + appointment.setAppointmentId(99L); + return appointment; + }); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(11L)); + + var response = appointmentService.createAppointment(request); + + assertEquals(99L, response.getAppointmentId()); + assertEquals(1L, response.getCustomerId()); + } + private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { Appointment appointment = new Appointment(); appointment.setAppointmentId(id); @@ -188,4 +273,14 @@ class AppointmentServiceTest { appointment.setPets(Set.of()); return appointment; } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new com.petshop.backend.security.AppPrincipal(userId, "user", role, 0), + "n/a", + List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) + ) + ); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java index a81faaff..299fcae9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java @@ -6,6 +6,7 @@ import java.util.List; public class AppointmentRequest { private List petIds; + private List customerPetIds; private Long customerId; private Long storeId; private Long serviceId; @@ -24,6 +25,14 @@ public class AppointmentRequest { this.petIds = petIds; } + public List getCustomerPetIds() { + return customerPetIds; + } + + public void setCustomerPetIds(List customerPetIds) { + this.customerPetIds = customerPetIds; + } + public Long getCustomerId() { return customerId; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java index 1d904bd0..c71dc3f3 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java @@ -12,6 +12,8 @@ public class AppointmentResponse { private Long serviceId; private java.util.List petNames; private java.util.List petIds; + private java.util.List customerPetNames; + private java.util.List customerPetIds; private String serviceName; private LocalDate appointmentDate; private LocalTime appointmentTime; @@ -84,6 +86,22 @@ public class AppointmentResponse { this.petIds = petIds; } + public java.util.List getCustomerPetNames() { + return customerPetNames; + } + + public void setCustomerPetNames(java.util.List customerPetNames) { + this.customerPetNames = customerPetNames; + } + + public java.util.List getCustomerPetIds() { + return customerPetIds; + } + + public void setCustomerPetIds(java.util.List customerPetIds) { + this.customerPetIds = customerPetIds; + } + public String getServiceName() { return serviceName; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index 30fcb0b8..ec0fe4d0 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -82,6 +82,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getCustomerPets(Long customerId) throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from customer pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getStores() throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/stores"); if (response == null || response.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index bd5dc392..e310c039 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -233,13 +233,18 @@ public class AppointmentController { } private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) { - Long petId = response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; + Long petId = response.getCustomerPetIds() != null && !response.getCustomerPetIds().isEmpty() + ? response.getCustomerPetIds().get(0) + : response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; + String petName = response.getCustomerPetNames() != null && !response.getCustomerPetNames().isEmpty() + ? String.join(", ", response.getCustomerPetNames()) + : String.join(", ", response.getPetNames()); return new AppointmentDTO( response.getAppointmentId().intValue(), response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, response.getCustomerName(), petId != null ? petId.intValue() : 0, - String.join(", ", response.getPetNames()), + petName, response.getServiceId() != null ? response.getServiceId().intValue() : 0, response.getServiceName(), response.getAppointmentDate().toString(), diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 932d10d5..c4b39ba7 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -50,6 +50,7 @@ public class AppointmentDialogController { private String mode = null; // Add | Edit private AppointmentDTO selectedAppointment = null; + private Long pendingPetSelectionId = null; private ObservableList statusList = FXCollections.observableArrayList( @@ -77,7 +78,6 @@ public class AppointmentDialogController { try { List services = DropdownApi.getInstance().getServices(); List customers = DropdownApi.getInstance().getCustomers(); - List pets = DropdownApi.getInstance().getPets(); Platform.runLater(() -> { if (services != null) { @@ -86,9 +86,6 @@ public class AppointmentDialogController { if (customers != null) { cbCustomer.setItems(FXCollections.observableArrayList(customers)); } - if (pets != null) { - cbPet.setItems(FXCollections.observableArrayList(pets)); - } syncSelectedAppointment(); }); } catch (Exception e) { @@ -103,6 +100,7 @@ public class AppointmentDialogController { }).start(); cbAppointmentStatus.setItems(statusList); + cbPet.setDisable(true); // Hours 9 AM - 5 PM for (int i = 9; i <= 17; i++) { @@ -157,6 +155,18 @@ public class AppointmentDialogController { } }); + cbCustomer.valueProperty().addListener((obs, oldValue, newValue) -> { + Long customerId = newValue != null ? newValue.getId() : null; + cbPet.setValue(null); + cbPet.setItems(FXCollections.observableArrayList()); + cbPet.setDisable(customerId == null); + if (customerId != null) { + loadCustomerPets(customerId); + } else { + pendingPetSelectionId = null; + } + }); + btnSave.setOnMouseClicked(this::buttonSaveClicked); btnCancel.setOnMouseClicked(this::closeStage); } @@ -199,11 +209,10 @@ public class AppointmentDialogController { }); cbCustomer.getItems().forEach(c -> { - if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) cbCustomer.setValue(c); - }); - - cbPet.getItems().forEach(p -> { - if (p.getId() != null && p.getId().longValue() == appt.getPetId()) cbPet.setValue(p); + if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) { + pendingPetSelectionId = (long) appt.getPetId(); + cbCustomer.setValue(c); + } }); } @@ -233,7 +242,7 @@ public class AppointmentDialogController { } AppointmentRequest request = new AppointmentRequest(); - request.setPetIds(Collections.singletonList(cbPet.getValue().getId())); + request.setCustomerPetIds(Collections.singletonList(cbPet.getValue().getId())); request.setCustomerId(cbCustomer.getValue().getId()); request.setStoreId(storeId); request.setServiceId(cbService.getValue().getId()); @@ -288,4 +297,34 @@ public class AppointmentDialogController { displayAppointmentDetails(selectedAppointment); } } + + private void loadCustomerPets(Long customerId) { + new Thread(() -> { + try { + List pets = DropdownApi.getInstance().getCustomerPets(customerId); + Platform.runLater(() -> { + cbPet.setItems(FXCollections.observableArrayList(pets)); + cbPet.setDisable(false); + if (pendingPetSelectionId != null) { + for (DropdownOption pet : cbPet.getItems()) { + if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) { + cbPet.setValue(pet); + break; + } + } + pendingPetSelectionId = null; + } + }); + } catch (Exception ex) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadCustomerPets", + ex, + "Loading customer pets for appointment dialog"); + cbPet.setDisable(true); + showError("Error loading pets for selected customer"); + }); + } + }).start(); + } } From 1079abf0c522391ae56a744ec6f242e15447607c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sat, 4 Apr 2026 16:23:28 -0600 Subject: [PATCH 046/137] Harden appointment dialog --- .../AppointmentDialogController.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index c4b39ba7..81b54cb9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -19,6 +19,7 @@ import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalTime; +import java.time.LocalDate; import java.util.Collections; import java.util.List; @@ -101,6 +102,11 @@ public class AppointmentDialogController { cbAppointmentStatus.setItems(statusList); cbPet.setDisable(true); + cbPet.setPromptText("Select a customer first"); + cbCustomer.setPromptText("Select a customer"); + cbService.setPromptText("Select a service"); + dpAppointmentDate.setValue(LocalDate.now().plusDays(1)); + cbAppointmentStatus.setValue("Booked"); // Hours 9 AM - 5 PM for (int i = 9; i <= 17; i++) { @@ -161,8 +167,10 @@ public class AppointmentDialogController { cbPet.setItems(FXCollections.observableArrayList()); cbPet.setDisable(customerId == null); if (customerId != null) { + cbPet.setPromptText("Loading customer pets..."); loadCustomerPets(customerId); } else { + cbPet.setPromptText("Select a customer first"); pendingPetSelectionId = null; } }); @@ -304,14 +312,25 @@ public class AppointmentDialogController { List pets = DropdownApi.getInstance().getCustomerPets(customerId); Platform.runLater(() -> { cbPet.setItems(FXCollections.observableArrayList(pets)); - cbPet.setDisable(false); + cbPet.setDisable(pets == null || pets.isEmpty()); + cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet"); if (pendingPetSelectionId != null) { + boolean matched = false; for (DropdownOption pet : cbPet.getItems()) { if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) { cbPet.setValue(pet); + matched = true; break; } } + if (!matched && selectedAppointment != null && selectedAppointment.getPetName() != null && !selectedAppointment.getPetName().isBlank()) { + DropdownOption legacy = new DropdownOption(); + legacy.setId(pendingPetSelectionId); + legacy.setLabel(selectedAppointment.getPetName() + " (legacy appointment pet)"); + cbPet.getItems().add(0, legacy); + cbPet.setValue(legacy); + cbPet.setDisable(false); + } pendingPetSelectionId = null; } }); @@ -322,6 +341,7 @@ public class AppointmentDialogController { ex, "Loading customer pets for appointment dialog"); cbPet.setDisable(true); + cbPet.setPromptText("Unable to load pets"); showError("Error loading pets for selected customer"); }); } From 313ec4a57b6a89177576ccd2d2ed4bfc92c8c2c7 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:54:29 -0600 Subject: [PATCH 047/137] Added enter send message and login for andriod feilds --- .../petstoremobile/activities/MainActivity.java | 10 ++++++++++ .../petstoremobile/fragments/ChatFragment.java | 11 +++++++++++ android/app/src/main/res/layout/activity_main.xml | 1 + android/app/src/main/res/layout/fragment_chat.xml | 1 + 4 files changed, 23 insertions(+) diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index 94a51ac3..7615b27e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.activities; import android.content.Intent; import android.os.Bundle; import android.util.Log; +import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; @@ -69,6 +70,15 @@ public class MainActivity extends AppCompatActivity { //clear login status tvLoginStatus.setText(""); + // Set editor action listener for password field to login on when enter is pressed + etPassword.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) { + btnLogin.performClick(); + return true; + } + return false; + }); + //Set click listener for login button btnLogin.setOnClickListener(v -> { //Get username and password from text fields diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index f52d5ea6..47ec7f3f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -8,6 +8,7 @@ import android.os.Bundle; import android.provider.OpenableColumns; import android.util.Log; import android.view.*; +import android.view.inputmethod.EditorInfo; import android.widget.*; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -121,6 +122,16 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ImageButton hamburger = view.findViewById(R.id.btnHamburger); hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + + // Set editor action listener for message field to send when enter is pressed + etMessage.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) { + btnSend.performClick(); + return true; + } + return false; + }); + //When the send button is clicked check if there is an attachment and send using the correct helper function btnSend.setOnClickListener(v -> { if (pendingAttachmentUri != null) { diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 9322e775..35692e3c 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -96,6 +96,7 @@ android:layout_height="wrap_content" android:hint="Enter password" android:inputType="textPassword" + android:imeOptions="actionDone" android:layout_marginBottom="24dp" android:textColor="@color/text_dark"/> diff --git a/android/app/src/main/res/layout/fragment_chat.xml b/android/app/src/main/res/layout/fragment_chat.xml index 786f53bc..4101f774 100644 --- a/android/app/src/main/res/layout/fragment_chat.xml +++ b/android/app/src/main/res/layout/fragment_chat.xml @@ -109,6 +109,7 @@ android:layout_weight="1" android:hint="Type a message..." android:inputType="text" + android:imeOptions="actionSend" android:layout_marginEnd="8dp" android:textColor="@color/text_dark"/> From 933a6bff6b378b0a13d7abc4e5004510616f1583 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:16:44 -0600 Subject: [PATCH 048/137] Added calendar view to adoptions in andriod --- .../listfragments/AdoptionFragment.java | 97 ++++++++++++++++--- .../src/main/res/layout/fragment_adoption.xml | 22 ++++- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 34e7d602..c655f78b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -1,10 +1,12 @@ package com.example.petstoremobile.fragments.listfragments; +import android.graphics.Color; import android.os.Bundle; import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -17,7 +19,14 @@ import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment; +import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.prolificinteractive.materialcalendarview.CalendarDay; +import com.prolificinteractive.materialcalendarview.CalendarMode; +import com.prolificinteractive.materialcalendarview.MaterialCalendarView; +import com.prolificinteractive.materialcalendarview.OnDateSelectedListener; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.*; import retrofit2.*; @@ -30,6 +39,11 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private SwipeRefreshLayout swipeRefresh; private EditText etSearch; private ImageButton hamburger; + private ImageButton btnToggleCalendarMode; + private MaterialCalendarView calendarView; + private CalendarDay selectedCalendarDay; + private boolean isMonthMode = false; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -38,10 +52,13 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop api = RetrofitClient.getAdoptionApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburgerAdoption); + calendarView = view.findViewById(R.id.calendarViewAdoption); + btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarModeAdoption); setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + setupCalendar(); loadAdoptions(); FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption); @@ -52,9 +69,57 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (lf != null) lf.openDrawer(); }); + btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); + return view; } + private void toggleCalendarMode() { + isMonthMode = !isMonthMode; + calendarView.state().edit() + .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) + .commit(); + } + + private void setupCalendar() { + calendarView.setOnDateChangedListener(new OnDateSelectedListener() { + @Override + public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) { + if (selected) { + if (date.equals(selectedCalendarDay)) { + selectedCalendarDay = null; + calendarView.clearSelection(); + } else { + selectedCalendarDay = date; + } + } else { + selectedCalendarDay = null; + } + filter(etSearch.getText().toString()); + } + }); + } + + private void updateCalendarDecorators() { + HashSet datesWithAdoptions = new HashSet<>(); + for (AdoptionDTO adoption : adoptionList) { + try { + if (adoption.getAdoptionDate() != null) { + Date date = dateFormat.parse(adoption.getAdoptionDate()); + if (date != null) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + datesWithAdoptions.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))); + } + } + } catch (ParseException e) { + Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate()); + } + } + calendarView.removeDecorators(); + calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); + } + private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); adapter = new AdoptionAdapter(filteredList, this); @@ -80,16 +145,25 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private void filter(String query) { filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(adoptionList); - } else { - String lower = query.toLowerCase(); - for (AdoptionDTO a : adoptionList) { - if ((a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lower)) - || (a.getPetName() != null && a.getPetName().toLowerCase().contains(lower)) - || (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lower))) { - filteredList.add(a); - } + String lowerQuery = query.toLowerCase(); + + String selectedDateString = null; + if (selectedCalendarDay != null) { + selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", + selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); + } + + for (AdoptionDTO a : adoptionList) { + boolean matchesSearch = query.isEmpty() || + (a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || + (a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)) || + (a.getAdoptionStatus() != null && a.getAdoptionStatus().toLowerCase().contains(lowerQuery)); + + boolean matchesDate = (selectedDateString == null) || + (a.getAdoptionDate() != null && a.getAdoptionDate().startsWith(selectedDateString)); + + if (matchesSearch && matchesDate) { + filteredList.add(a); } } adapter.notifyDataSetChanged(); @@ -97,13 +171,14 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private void loadAdoptions() { if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - api.getAllAdoptions(0, 100).enqueue(new Callback>() { + api.getAllAdoptions(0, 500).enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (swipeRefresh != null) swipeRefresh.setRefreshing(false); if (r.isSuccessful() && r.body() != null) { adoptionList.clear(); adoptionList.addAll(r.body().getContent()); + updateCalendarDecorators(); filter(etSearch != null ? etSearch.getText().toString() : ""); } else { Toast.makeText(getContext(), "Failed to load adoptions", Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/res/layout/fragment_adoption.xml b/android/app/src/main/res/layout/fragment_adoption.xml index 492b9236..5bc95c38 100644 --- a/android/app/src/main/res/layout/fragment_adoption.xml +++ b/android/app/src/main/res/layout/fragment_adoption.xml @@ -28,15 +28,35 @@ android:contentDescription="Open menu"/> + + + + Date: Sat, 4 Apr 2026 18:15:05 -0600 Subject: [PATCH 049/137] integrated hilt so we dont have to manually pass context and inject retrofit in andriod --- android/app/build.gradle.kts | 4 + .../petstoremobile/PetStoreApplication.java | 3 +- .../activities/HomeActivity.java | 6 +- .../activities/MainActivity.java | 24 ++- .../petstoremobile/adapters/PetAdapter.java | 28 ++- .../adapters/ProductAdapter.java | 28 ++- .../api/auth/AuthInterceptor.java | 6 +- .../petstoremobile/api/auth/TokenManager.java | 22 +-- .../petstoremobile/di/NetworkModule.java | 176 ++++++++++++++++++ .../fragments/ChatFragment.java | 31 +-- .../fragments/ListFragment.java | 10 +- .../fragments/ProfileFragment.java | 23 ++- .../listfragments/AdoptionFragment.java | 9 +- .../listfragments/AppointmentFragment.java | 16 +- .../listfragments/InventoryFragment.java | 16 +- .../fragments/listfragments/PetFragment.java | 14 +- .../listfragments/ProductFragment.java | 11 +- .../ProductSupplierFragment.java | 11 +- .../listfragments/PurchaseOrderFragment.java | 11 +- .../fragments/listfragments/SaleFragment.java | 8 + .../listfragments/ServiceFragment.java | 8 +- .../listfragments/SupplierFragment.java | 8 +- .../AdoptionDetailFragment.java | 21 ++- .../AppointmentDetailFragment.java | 29 ++- .../InventoryDetailFragment.java | 12 +- .../detailfragments/PetDetailFragment.java | 12 +- .../ProductDetailFragment.java | 26 ++- .../ProductSupplierDetailFragment.java | 21 ++- .../PurchaseOrderDetailFragment.java | 3 + .../detailfragments/RefundDetailFragment.java | 8 + .../ServiceDetailFragment.java | 12 +- .../SupplierDetailFragment.java | 12 +- .../PetProfileFragment.java | 13 +- .../services/ChatNotificationService.java | 27 +-- .../websocket/StompChatManager.java | 23 +-- android/build.gradle.kts | 1 + android/gradle/libs.versions.toml | 5 +- 37 files changed, 504 insertions(+), 194 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a6d27404..ef712a40 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -2,6 +2,7 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) + alias(libs.plugins.hilt) } val localProperties = Properties().apply { @@ -60,6 +61,9 @@ dependencies { implementation(libs.activity) implementation(libs.constraintlayout) + implementation(libs.hilt.android) + annotationProcessor(libs.hilt.compiler) + implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") diff --git a/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java index b041d54d..75def31b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java +++ b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java @@ -1,8 +1,9 @@ package com.example.petstoremobile; import android.app.Application; -import com.example.petstoremobile.api.auth.TokenManager; +import dagger.hilt.android.HiltAndroidApp; +@HiltAndroidApp public class PetStoreApplication extends Application { @Override public void onCreate() { diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index aea1f427..513c3683 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -10,7 +10,6 @@ import android.util.Log; import androidx.activity.EdgeToEdge; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; @@ -25,6 +24,9 @@ import com.example.petstoremobile.fragments.ProfileFragment; import com.example.petstoremobile.services.ChatNotificationService; import com.google.android.material.bottomnavigation.BottomNavigationView; +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class HomeActivity extends AppCompatActivity { private BottomNavigationView bottomNav; @@ -125,4 +127,4 @@ public class HomeActivity extends AppCompatActivity { .replace(R.id.fragment_container, fragment) .commit(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index 7615b27e..b4b00db4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -18,15 +18,19 @@ import androidx.core.view.WindowInsetsCompat; import com.example.petstoremobile.R; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.dtos.UserDTO; +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; //The login screen activity +@AndroidEntryPoint public class MainActivity extends AppCompatActivity { private EditText etUser; @@ -34,6 +38,10 @@ public class MainActivity extends AppCompatActivity { private Button btnLogin; private TextView tvLoginStatus; + @Inject AuthApi authApi; + @Inject TokenManager tokenManager; + @Inject @Named("baseUrl") String baseUrl; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -41,7 +49,6 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); // Check if user is already logged in - TokenManager tokenManager = TokenManager.getInstance(this); if (tokenManager.isLoggedIn()) { if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) { // If a customer somehow remained logged in, clear them out @@ -92,8 +99,6 @@ public class MainActivity extends AppCompatActivity { return; } - AuthApi authApi = RetrofitClient.getAuthApi(this); - //Call login from api and get response authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback() { @Override @@ -109,21 +114,20 @@ public class MainActivity extends AppCompatActivity { } //save login data in shared preferences - TokenManager.getInstance(MainActivity.this).saveLoginData( + tokenManager.saveLoginData( response.body().getToken(), response.body().getUsername(), role ); //fetch user id from api then login to home activity - RetrofitClient.getAuthApi(MainActivity.this).getMe() + authApi.getMe() .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null) { - TokenManager.getInstance(MainActivity.this) - .saveUserId(response.body().getId()); + tokenManager.saveUserId(response.body().getId()); } Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); @@ -169,7 +173,7 @@ public class MainActivity extends AppCompatActivity { if (t instanceof java.net.ConnectException || t instanceof java.net.SocketTimeoutException || t instanceof java.net.UnknownHostException) { - errorMessage = "Cannot connect to server at " + RetrofitClient.BASE_URL + + errorMessage = "Cannot connect to server at " + baseUrl + ". Please check if the backend is running."; } else if (t instanceof java.io.IOException) { errorMessage = "Network error. Please check your connection."; @@ -183,4 +187,4 @@ public class MainActivity extends AppCompatActivity { }); }); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 166b0d1d..d5008aff 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -13,7 +13,6 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.PetDTO; import java.util.List; @@ -21,6 +20,7 @@ public class PetAdapter extends RecyclerView.Adapter { private List petList; private OnPetClickListener petClickListener; + private String baseUrl; // Interface for pet click on recycler view public interface OnPetClickListener { @@ -33,6 +33,10 @@ public class PetAdapter extends RecyclerView.Adapter { this.petClickListener = petClickListener; } + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; @@ -83,15 +87,19 @@ public class PetAdapter extends RecyclerView.Adapter { } // Load pet image using Glide with circle crop - String imageUrl = RetrofitClient.BASE_URL + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); - Glide.with(holder.itemView.getContext()) - .load(imageUrl) - .circleCrop() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .placeholder(R.drawable.placeholder) - .error(R.drawable.placeholder) - .into(holder.ivPetProfile); + if (baseUrl != null) { + String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); + Glide.with(holder.itemView.getContext()) + .load(imageUrl) + .circleCrop() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(R.drawable.placeholder) + .error(R.drawable.placeholder) + .into(holder.ivPetProfile); + } else { + holder.ivPetProfile.setImageResource(R.drawable.placeholder); + } //when a row is clicked, open the detail view holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index a44ec993..5bcc4d3a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -10,7 +10,6 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ProductApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ProductDTO; import java.util.List; @@ -18,6 +17,7 @@ public class ProductAdapter extends RecyclerView.Adapter productList; private OnProductClickListener listener; + private String baseUrl; public interface OnProductClickListener { void onProductClick(int position); @@ -28,6 +28,10 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java index dd17fffd..02bbe3c0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java @@ -1,7 +1,5 @@ package com.example.petstoremobile.api.auth; -import android.content.Context; - import androidx.annotation.NonNull; import java.io.IOException; @@ -15,8 +13,8 @@ public class AuthInterceptor implements Interceptor { private final TokenManager tokenManager; - public AuthInterceptor(Context context) { - this.tokenManager = TokenManager.getInstance(context); + public AuthInterceptor(TokenManager tokenManager) { + this.tokenManager = tokenManager; } @NonNull diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java index b0f90508..aa9ab363 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java @@ -3,7 +3,12 @@ package com.example.petstoremobile.api.auth; import android.content.Context; import android.content.SharedPreferences; -//Store login token in shared preferences +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.hilt.android.qualifiers.ApplicationContext; + +@Singleton public class TokenManager { private static final String TOKEN_KEY = "token"; private static final String USERNAME_KEY = "username"; @@ -11,20 +16,13 @@ public class TokenManager { private static final String PREFS_NAME = "auth_prefs"; private static final String USER_ID_KEY = "user_id"; - private static TokenManager instance; private SharedPreferences prefs; - private TokenManager(Context context) { + @Inject + public TokenManager(@ApplicationContext Context context) { prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } - public static TokenManager getInstance(Context context) { - if (instance == null) { - instance = new TokenManager(context); - } - return instance; - } - //save login data after login public void saveLoginData(String token, String username, String role) { prefs.edit() @@ -65,6 +63,4 @@ public class TokenManager { public void clearLoginData() { prefs.edit().clear().apply(); } - - -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java new file mode 100644 index 00000000..0311840d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -0,0 +1,176 @@ +package com.example.petstoremobile.di; + +import android.content.Context; +import android.os.Build; + +import com.example.petstoremobile.BuildConfig; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.api.auth.AuthApi; +import com.example.petstoremobile.api.auth.AuthInterceptor; +import com.example.petstoremobile.api.auth.TokenManager; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import dagger.hilt.InstallIn; +import dagger.hilt.android.qualifiers.ApplicationContext; +import dagger.hilt.components.SingletonComponent; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +//Module to provide dependencies injection for the api +@Module +@InstallIn(SingletonComponent.class) +public class NetworkModule { + + @Provides + @Singleton + @Named("baseUrl") + public static String provideBaseUrl() { + return isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL; + } + + // Check if the device is an emulator + private static boolean isEmulator() { + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_gphone") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")); + } + + @Provides + @Singleton + public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) { + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + return new OkHttpClient.Builder() + .addInterceptor(interceptor) + .addInterceptor(new AuthInterceptor(tokenManager)) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + } + + //build the retrofit instance + @Provides + @Singleton + public static Retrofit provideRetrofit(@Named("baseUrl") String baseUrl, OkHttpClient client) { + return new Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build(); + } + + //associate the api with the retrofit instance + @Provides + @Singleton + public static PetApi providePetApi(Retrofit retrofit) { + return retrofit.create(PetApi.class); + } + + @Provides + @Singleton + public static ServiceApi provideServiceApi(Retrofit retrofit) { + return retrofit.create(ServiceApi.class); + } + + @Provides + @Singleton + public static SupplierApi provideSupplierApi(Retrofit retrofit) { + return retrofit.create(SupplierApi.class); + } + + @Provides + @Singleton + public static AdoptionApi provideAdoptionApi(Retrofit retrofit) { + return retrofit.create(AdoptionApi.class); + } + + @Provides + @Singleton + public static AppointmentApi provideAppointmentApi(Retrofit retrofit) { + return retrofit.create(AppointmentApi.class); + } + + @Provides + @Singleton + public static ProductApi provideProductApi(Retrofit retrofit) { + return retrofit.create(ProductApi.class); + } + + @Provides + @Singleton + public static SaleApi provideSaleApi(Retrofit retrofit) { + return retrofit.create(SaleApi.class); + } + + @Provides + @Singleton + public static PurchaseOrderApi providePurchaseOrderApi(Retrofit retrofit) { + return retrofit.create(PurchaseOrderApi.class); + } + + @Provides + @Singleton + public static ProductSupplierApi provideProductSupplierApi(Retrofit retrofit) { + return retrofit.create(ProductSupplierApi.class); + } + + @Provides + @Singleton + public static InventoryApi provideInventoryApi(Retrofit retrofit) { + return retrofit.create(InventoryApi.class); + } + + @Provides + @Singleton + public static AuthApi provideAuthApi(Retrofit retrofit) { + return retrofit.create(AuthApi.class); + } + + @Provides + @Singleton + public static ChatApi provideChatApi(Retrofit retrofit) { + return retrofit.create(ChatApi.class); + } + + @Provides + @Singleton + public static CustomerApi provideCustomerApi(Retrofit retrofit) { + return retrofit.create(CustomerApi.class); + } + + @Provides + @Singleton + public static MessageApi provideMessageApi(Retrofit retrofit) { + return retrofit.create(MessageApi.class); + } + + @Provides + @Singleton + public static StoreApi provideStoreApi(Retrofit retrofit) { + return retrofit.create(StoreApi.class); + } + + @Provides + @Singleton + public static CategoryApi provideCategoryApi(Retrofit retrofit) { + return retrofit.create(CategoryApi.class); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 47ec7f3f..a30f1f03 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -26,7 +26,6 @@ import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.api.MessageApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.MessageDTO; @@ -36,10 +35,17 @@ import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.websocket.StompChatManager; + import java.util.*; import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, StompChatManager.ConversationListener, StompChatManager.ConnectionListener { @@ -70,9 +76,11 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private Uri pendingAttachmentUri; // APIs - private ChatApi chatApi; - private CustomerApi customerApi; - private MessageApi messageApi; + @Inject ChatApi chatApi; + @Inject CustomerApi customerApi; + @Inject MessageApi messageApi; + @Inject TokenManager tokenManager; + @Inject @Named("baseUrl") String baseUrl; // chat private Long currentUserId; @@ -103,10 +111,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis View view = inflater.inflate(R.layout.fragment_chat, container, false); - chatApi = RetrofitClient.getChatApi(requireContext()); - customerApi = RetrofitClient.getCustomerApi(requireContext()); - messageApi = RetrofitClient.getMessageApi(requireContext()); - drawerLayout = view.findViewById(R.id.chatDrawerLayout); rvChatList = view.findViewById(R.id.rvChatList); rvMessages = view.findViewById(R.id.rvMessages); @@ -169,16 +173,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //Helper function to load token and user id then connect to websocket private void loadInitialData() { - TokenManager tm = TokenManager.getInstance(requireContext()); - String token = tm.getToken(); - currentUserId = tm.getUserId(); - String role = tm.getRole(); + String token = tokenManager.getToken(); + currentUserId = tokenManager.getUserId(); + String role = tokenManager.getRole(); messageAdapter.setCurrentUserId(currentUserId); // if token exist then connect to websocket if (token != null) { - stompChatManager = new StompChatManager(token, role); + stompChatManager = new StompChatManager(token, role, baseUrl); stompChatManager.setMessageListener(this); stompChatManager.setConversationListener(this); stompChatManager.setConnectionListener(this); @@ -561,4 +564,4 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ChatNotificationService.activeConversationIdInUi = null; if (stompChatManager != null) stompChatManager.disconnect(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index b63b42b1..97e5100c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -26,7 +26,12 @@ import com.example.petstoremobile.fragments.listfragments.ProductSupplierFragmen import com.example.petstoremobile.fragments.listfragments.PurchaseOrderFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + //The Fragment for the displaying the list of entities to be viewed +@AndroidEntryPoint public class ListFragment extends Fragment { private DrawerLayout drawerLayout; @@ -37,6 +42,7 @@ public class ListFragment extends Fragment { private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale; + @Inject TokenManager tokenManager; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -58,7 +64,7 @@ public class ListFragment extends Fragment { // Check user role and restrict access for STAFF - String role = TokenManager.getInstance(requireContext()).getRole(); + String role = tokenManager.getRole(); if ("STAFF".equalsIgnoreCase(role)) { drawerSuppliers.setVisibility(View.GONE); drawerInventory.setVisibility(View.GONE); @@ -178,4 +184,4 @@ public class ListFragment extends Fragment { .addToBackStack(null) .commit(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index b29c038d..f470ab31 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -32,7 +32,6 @@ import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.activities.MainActivity; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.ErrorResponse; @@ -49,6 +48,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -56,6 +59,7 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class ProfileFragment extends Fragment { //initialize the view/controls @@ -65,6 +69,10 @@ public class ProfileFragment extends Fragment { private UserDTO currentUser; private boolean hasImage = false; + @Inject AuthApi authApi; + @Inject TokenManager tokenManager; + @Inject @Named("baseUrl") String baseUrl; + //Initialize the launchers for camera and gallery private ActivityResultLauncher galleryLauncher; private ActivityResultLauncher cameraLauncher; @@ -251,7 +259,7 @@ public class ProfileFragment extends Fragment { Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class); requireContext().stopService(serviceIntent); - TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login + tokenManager.clearLoginData(); // clear the token for next login //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen Intent intent = new Intent(getActivity(), MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); @@ -275,8 +283,6 @@ public class ProfileFragment extends Fragment { //Helper function to call the backend to get profile data and load it to the view private void loadProfileData() { - AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); - authApi.getMe().enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -291,8 +297,8 @@ public class ProfileFragment extends Fragment { tvProfileRole.setText(currentUser.getRole()); // get the avatar endpoint to load profile image and the token for authorization - String avatarUrl = RetrofitClient.BASE_URL + AuthApi.AVATAR_FILE_PATH; - String token = TokenManager.getInstance(requireContext()).getToken(); + String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; + String token = tokenManager.getToken(); if (token != null) { // Create GlideUrl with token to fetch the image @@ -354,7 +360,6 @@ public class ProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); //Call the backend to upload the avatar - AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); authApi.uploadAvatar(body).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -380,7 +385,6 @@ public class ProfileFragment extends Fragment { } private void deleteAvatar() { - AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); authApi.deleteAvatar().enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -423,7 +427,6 @@ public class ProfileFragment extends Fragment { //Helper function to update a profile field in the backend private void updateProfileField(String fieldName, String value) { - AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); Map updates = new HashMap<>(); updates.put(fieldName, value); @@ -456,4 +459,4 @@ public class ProfileFragment extends Fragment { } }); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index c655f78b..06757a32 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -14,7 +14,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; import com.example.petstoremobile.api.AdoptionApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; @@ -28,14 +27,19 @@ import com.prolificinteractive.materialcalendarview.OnDateSelectedListener; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { private List adoptionList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private AdoptionAdapter adapter; - private AdoptionApi api; + @Inject AdoptionApi api; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; private ImageButton hamburger; @@ -50,7 +54,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_adoption, container, false); - api = RetrofitClient.getAdoptionApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburgerAdoption); calendarView = view.findViewById(R.id.calendarViewAdoption); btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarModeAdoption); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 49da714c..83115a3b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -23,7 +23,6 @@ import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.ServiceApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.PageResponse; @@ -46,10 +45,14 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { private List appointmentList = new ArrayList<>(); @@ -58,7 +61,10 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private List serviceList = new ArrayList<>(); private AppointmentAdapter adapter; - private AppointmentApi api; + @Inject AppointmentApi api; + @Inject PetApi petApi; + @Inject ServiceApi serviceApi; + private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; private ImageButton hamburger; @@ -73,7 +79,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_appointment, container, false); - api = RetrofitClient.getAppointmentApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburger); calendarView = view.findViewById(R.id.calendarView); btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode); @@ -267,7 +272,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. // Load Pets private void loadPets() { - PetApi petApi = RetrofitClient.getPetApi(requireContext()); petApi.getAllPets(0,100).enqueue(new Callback>() { @Override @@ -290,8 +294,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. // Load Services private void loadServices() { - ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); - serviceApi.getAllServices(0,100).enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { @@ -331,4 +333,4 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 37e533ee..ce93ca54 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -10,7 +10,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -28,7 +27,6 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.api.InventoryApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; @@ -40,10 +38,14 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { private static final String TAG = "InventoryFragment"; @@ -52,8 +54,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private final List inventoryList = new ArrayList<>(); private final List categoryList = new ArrayList<>(); private InventoryAdapter adapter; - private InventoryApi inventoryApi; - private CategoryApi categoryApi; + @Inject InventoryApi inventoryApi; + @Inject CategoryApi categoryApi; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; @@ -83,9 +85,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory, container, false); - inventoryApi = RetrofitClient.getInventoryApi(requireContext()); - categoryApi = RetrofitClient.getCategoryApi(requireContext()); - hamburger = view.findViewById(R.id.btnHamburger); btnBulkDelete = view.findViewById(R.id.btnBulkDelete); tvSelectionCount = view.findViewById(R.id.tvSelectionCount); @@ -175,8 +174,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchInventory); etSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 4c8effcf..693e261f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -14,7 +14,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageButton; import android.widget.Spinner; @@ -24,7 +23,6 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; @@ -35,16 +33,22 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private List petList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private ImageButton hamburger; private PetAdapter adapter; - private PetApi api; + @Inject PetApi api; + @Inject @Named("baseUrl") String baseUrl; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; private Spinner spinnerStatus; @@ -55,9 +59,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_pet, container, false); - //get retrofit - api = RetrofitClient.getPetApi(requireContext()); - hamburger = view.findViewById(R.id.btnHamburger); setupRecyclerView(view); @@ -231,6 +232,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); adapter = new PetAdapter(filteredList, this); + adapter.setBaseUrl(baseUrl); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index e8b29611..ce15a7e8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -11,15 +11,20 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { private List productList = new ArrayList<>(); @@ -28,6 +33,8 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + @Inject ProductApi api; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -93,7 +100,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private void loadProducts() { if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 100) + api.getAllProducts(null, 0, 100) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 4ff88f16..0f76041e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -11,15 +11,20 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.api.ProductSupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class ProductSupplierFragment extends Fragment implements ProductSupplierAdapter.OnProductSupplierClickListener { @@ -29,6 +34,8 @@ public class ProductSupplierFragment extends Fragment private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + @Inject ProductSupplierApi api; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -92,7 +99,7 @@ public class ProductSupplierFragment extends Fragment private void loadData() { if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - RetrofitClient.getProductSupplierApi(requireContext()).getAllProductSuppliers(0, 100) + api.getAllProductSuppliers(0, 100) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 76527f09..4840b549 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -11,14 +11,19 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PurchaseOrderAdapter; -import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.api.PurchaseOrderApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.PurchaseOrderDetailFragment; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class PurchaseOrderFragment extends Fragment implements PurchaseOrderAdapter.OnPurchaseOrderClickListener { @@ -28,6 +33,8 @@ public class PurchaseOrderFragment extends Fragment private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + @Inject PurchaseOrderApi api; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -94,7 +101,7 @@ public class PurchaseOrderFragment extends Fragment private void loadData() { if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - RetrofitClient.getPurchaseOrderApi(requireContext()).getAllPurchaseOrders(0, 100) + api.getAllPurchaseOrders(0, 100) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index c813aa5a..74b8c4ef 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -15,12 +15,18 @@ import android.widget.ImageButton; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; +import com.example.petstoremobile.api.SaleApi; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundDetailFragment; import com.example.petstoremobile.models.Sale; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { private List saleList = new ArrayList<>(); @@ -30,6 +36,8 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis private EditText etSearch; private ImageButton btnHamburger; + @Inject SaleApi api; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 1f204114..afa793de 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -19,7 +19,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ServiceAdapter; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; @@ -30,17 +29,21 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { private List serviceList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private ServiceAdapter adapter; private ImageButton hamburger; - private ServiceApi api; + @Inject ServiceApi api; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; @@ -50,7 +53,6 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_service, container, false); - api = RetrofitClient.getServiceApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburger); setupRecyclerView(view); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 0d75da78..66b72b8b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -19,7 +19,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; @@ -30,17 +29,21 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { private List supplierList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private SupplierAdapter adapter; private ImageButton hamburger; - private SupplierApi api; + @Inject SupplierApi api; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; @@ -50,7 +53,6 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_supplier, container, false); - api = RetrofitClient.getSupplierApi(requireContext()); hamburger = view.findViewById(R.id.btnHamburger); setupRecyclerView(view); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 47733da2..512427d5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -14,8 +14,13 @@ import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class AdoptionDetailFragment extends Fragment { private TextView tvMode, tvAdoptionId; @@ -33,6 +38,10 @@ public class AdoptionDetailFragment extends Fragment { private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; + @Inject AdoptionApi adoptionApi; + @Inject PetApi petApi; + @Inject CustomerApi customerApi; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -84,7 +93,7 @@ public class AdoptionDetailFragment extends Fragment { } private void loadPets() { - RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) + petApi.getAllPets(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { @@ -115,7 +124,7 @@ public class AdoptionDetailFragment extends Fragment { } private void loadCustomers() { - RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) + customerApi.getAllCustomers(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { @@ -201,11 +210,10 @@ public class AdoptionDetailFragment extends Fragment { + " customerId=" + customer.getCustomerId() + " date=" + date + " status=" + status); - AdoptionApi api = RetrofitClient.getAdoptionApi(requireContext()); if (isEditing) { - api.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated")); + adoptionApi.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated")); } else { - api.createAdoption(dto).enqueue(simpleCallback("Saved")); + adoptionApi.createAdoption(dto).enqueue(simpleCallback("Saved")); } } @@ -237,8 +245,7 @@ public class AdoptionDetailFragment extends Fragment { new AlertDialog.Builder(requireContext()) .setTitle("Delete Adoption?") .setPositiveButton("Yes", (d, w) -> - RetrofitClient.getAdoptionApi(requireContext()) - .deleteAdoption(adoptionId) + adoptionApi.deleteAdoption(adoptionId) .enqueue(new Callback() { public void onResponse(Call c, Response r) { navigateBack(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index eaabc061..51e37666 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -14,8 +14,13 @@ import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class AppointmentDetailFragment extends Fragment { private TextView tvMode, tvAppointmentId; @@ -41,6 +46,12 @@ public class AppointmentDetailFragment extends Fragment { private final Integer[] MINUTES = {0,15,30,45}; private final String[] STATUSES = {"Booked","Completed","Cancelled"}; + @Inject AppointmentApi appointmentApi; + @Inject PetApi petApi; + @Inject ServiceApi serviceApi; + @Inject CustomerApi customerApi; + @Inject StoreApi storeApi; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); @@ -107,7 +118,7 @@ public class AppointmentDetailFragment extends Fragment { } private void loadPets() { - RetrofitClient.getPetApi(requireContext()).getAllPets(0, 200) + petApi.getAllPets(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { @@ -137,7 +148,7 @@ public class AppointmentDetailFragment extends Fragment { } private void loadServices() { - RetrofitClient.getServiceApi(requireContext()).getAllServices(0, 200) + serviceApi.getAllServices(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { @@ -167,7 +178,7 @@ public class AppointmentDetailFragment extends Fragment { } private void loadCustomers() { - RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) + customerApi.getAllCustomers(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { @@ -198,7 +209,7 @@ public class AppointmentDetailFragment extends Fragment { } private void loadStores() { - RetrofitClient.getStoreApi(requireContext()).getAllStores(0, 50) + storeApi.getAllStores(0, 50) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { @@ -228,7 +239,7 @@ public class AppointmentDetailFragment extends Fragment { } private void loadAllAppointments() { - RetrofitClient.getAppointmentApi(requireContext()).getAllAppointments(0, 500) + appointmentApi.getAllAppointments(0, 500) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) @@ -350,11 +361,10 @@ public class AppointmentDetailFragment extends Fragment { + " petId=" + pet.getPetId() + " date=" + date + " time=" + time); - AppointmentApi api = RetrofitClient.getAppointmentApi(requireContext()); if (isEditing) { - api.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated")); + appointmentApi.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated")); } else { - api.createAppointment(dto).enqueue(simpleCallback("Saved")); + appointmentApi.createAppointment(dto).enqueue(simpleCallback("Saved")); } } @@ -426,8 +436,7 @@ public class AppointmentDetailFragment extends Fragment { new AlertDialog.Builder(requireContext()) .setTitle("Delete Appointment?") .setPositiveButton("Yes", (d, w) -> - RetrofitClient.getAppointmentApi(requireContext()) - .deleteAppointment(appointmentId) + appointmentApi.deleteAppointment(appointmentId) .enqueue(new Callback() { public void onResponse(Call c, Response r) { navigateBack(); } public void onFailure(Call c, Throwable t) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index f98351e3..c1bb052d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -21,7 +21,6 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.api.ProductApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; @@ -32,10 +31,14 @@ import com.example.petstoremobile.fragments.listfragments.InventoryFragment; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class InventoryDetailFragment extends Fragment { private TextView tvMode, tvInventoryId, tvProductInfo; @@ -43,8 +46,8 @@ public class InventoryDetailFragment extends Fragment { private android.widget.EditText etQuantity; private Button btnSave, btnDelete, btnBack; - private InventoryApi inventoryApi; - private ProductApi productApi; + @Inject InventoryApi inventoryApi; + @Inject ProductApi productApi; private InventoryFragment inventoryFragment; private boolean isEditing = false; @@ -70,9 +73,6 @@ public class InventoryDetailFragment extends Fragment { Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); - inventoryApi = RetrofitClient.getInventoryApi(requireContext()); - productApi = RetrofitClient.getProductApi(requireContext()); - initViews(view); setupProductSearch(); handleArguments(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index c37558a7..7f9a44f1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -22,17 +22,20 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.PetFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class PetDetailFragment extends Fragment { private TextView tvMode, tvPetId; @@ -43,6 +46,8 @@ public class PetDetailFragment extends Fragment { private boolean isEditing = false; private PetFragment petFragment; + @Inject PetApi petApi; + //set the pet fragment to the parent so we refer back to pet view when save or delete is done public void setPetFragment(PetFragment fragment) { this.petFragment = fragment; @@ -92,8 +97,6 @@ public class PetDetailFragment extends Fragment { petDTO.setPetPrice(priceStr); petDTO.setPetStatus(status); - PetApi petApi = RetrofitClient.getPetApi(requireContext()); - //check if the pet is being edited or added if (isEditing) { // Update existing pet @@ -148,7 +151,6 @@ public class PetDetailFragment extends Fragment { .setTitle("Delete Pet") .setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?") .setPositiveButton("Delete", (dialog, which) -> { - PetApi petApi = RetrofitClient.getPetApi(requireContext()); //if they say yes then delete the pet petApi.deletePet((long) petId).enqueue(new Callback() { @Override @@ -242,4 +244,4 @@ public class PetDetailFragment extends Fragment { spinnerPetStatus.setAdapter(adapter); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 40bdc91b..228f2c01 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -29,11 +29,17 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.math.BigDecimal; import java.util.*; + +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; import retrofit2.*; +@AndroidEntryPoint public class ProductDetailFragment extends Fragment { private TextView tvMode, tvProductId; @@ -50,6 +56,10 @@ public class ProductDetailFragment extends Fragment { private List categoryList = new ArrayList<>(); private Uri photoUri; + @Inject ProductApi productApi; + @Inject CategoryApi categoryApi; + @Inject @Named("baseUrl") String baseUrl; + private ActivityResultLauncher galleryLauncher; private ActivityResultLauncher cameraLauncher; private ActivityResultLauncher permissionLauncher; @@ -155,7 +165,7 @@ public class ProductDetailFragment extends Fragment { // Helper function to remove the photo private void removePhoto() { if (isEditing) { - RetrofitClient.getProductApi(requireContext()).deleteProductImage(prodId) + productApi.deleteProductImage(prodId) .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -187,7 +197,7 @@ public class ProductDetailFragment extends Fragment { } private void loadCategories() { - RetrofitClient.getCategoryApi(requireContext()).getAllCategories(0, 100) + categoryApi.getAllCategories(0, 100) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { @@ -243,7 +253,7 @@ public class ProductDetailFragment extends Fragment { //load the product image from the backend private void loadProductImage() { - String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); Glide.with(this) .load(imageUrl) .diskCacheStrategy(DiskCacheStrategy.NONE) @@ -262,7 +272,7 @@ public class ProductDetailFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - RetrofitClient.getProductApi(requireContext()).uploadProductImage(prodId, body) + productApi.uploadProductImage(prodId, body) .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -328,11 +338,10 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - ProductApi api = RetrofitClient.getProductApi(requireContext()); if (isEditing) { - api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); + productApi.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); } else { - api.createProduct(dto).enqueue(new Callback() { + productApi.createProduct(dto).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null) { @@ -375,8 +384,7 @@ public class ProductDetailFragment extends Fragment { new AlertDialog.Builder(requireContext()) .setTitle("Delete Product?") .setPositiveButton("Yes", (d, w) -> - RetrofitClient.getProductApi(requireContext()) - .deleteProduct(prodId) + productApi.deleteProduct(prodId) .enqueue(new Callback() { public void onResponse(Call c, Response r) { navigateBack(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 2bd47432..909df42c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -14,8 +14,13 @@ import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; import java.math.BigDecimal; import java.util.*; + +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +@AndroidEntryPoint public class ProductSupplierDetailFragment extends Fragment { private TextView tvMode; @@ -32,6 +37,10 @@ public class ProductSupplierDetailFragment extends Fragment { private List productList = new ArrayList<>(); private List supplierList = new ArrayList<>(); + @Inject ProductSupplierApi productSupplierApi; + @Inject ProductApi productApi; + @Inject SupplierApi supplierApi; + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -62,7 +71,7 @@ public class ProductSupplierDetailFragment extends Fragment { } private void loadProducts() { - RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200) + productApi.getAllProducts(null, 0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { @@ -93,7 +102,7 @@ public class ProductSupplierDetailFragment extends Fragment { } private void loadSuppliers() { - RetrofitClient.getSupplierApi(requireContext()).getAllSuppliers(0, 200) + supplierApi.getAllSuppliers(0, 200) .enqueue(new Callback>() { public void onResponse(Call> c, Response> r) { @@ -164,12 +173,11 @@ public class ProductSupplierDetailFragment extends Fragment { ProductSupplierDTO dto = new ProductSupplierDTO( product.getProdId(), supplier.getSupId(), cost); - ProductSupplierApi api = RetrofitClient.getProductSupplierApi(requireContext()); if (isEditing) { - api.updateProductSupplier(editProductId, editSupplierId, dto) + productSupplierApi.updateProductSupplier(editProductId, editSupplierId, dto) .enqueue(simpleCallback("Updated")); } else { - api.createProductSupplier(dto).enqueue(simpleCallback("Saved")); + productSupplierApi.createProductSupplier(dto).enqueue(simpleCallback("Saved")); } } @@ -200,8 +208,7 @@ public class ProductSupplierDetailFragment extends Fragment { new AlertDialog.Builder(requireContext()) .setTitle("Delete?") .setPositiveButton("Yes", (d, w) -> - RetrofitClient.getProductSupplierApi(requireContext()) - .deleteProductSupplier(editProductId, editSupplierId) + productSupplierApi.deleteProductSupplier(editProductId, editSupplierId) .enqueue(new Callback() { public void onResponse(Call c, Response r) { navigateBack(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index 47dae4d8..eec648e2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -9,6 +9,9 @@ import androidx.fragment.app.Fragment; import com.example.petstoremobile.R; import com.example.petstoremobile.fragments.ListFragment; +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class PurchaseOrderDetailFragment extends Fragment { private TextView tvId, tvSupplier, tvDate, tvStatus; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index 41b8f3b2..190c89c5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -13,11 +13,17 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; +import com.example.petstoremobile.api.SaleApi; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class RefundDetailFragment extends Fragment { private EditText etRefundSaleId, etRefundReason; @@ -27,6 +33,8 @@ public class RefundDetailFragment extends Fragment { private int saleId; private SaleFragment saleFragment; + @Inject SaleApi saleApi; + public void setSaleFragment(SaleFragment fragment) { this.saleFragment = fragment; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index c7b0a4c5..4176fa38 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -15,7 +15,6 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.ListFragment; @@ -23,10 +22,14 @@ import com.example.petstoremobile.fragments.listfragments.ServiceFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class ServiceDetailFragment extends Fragment { private TextView tvMode, tvServiceId; @@ -36,6 +39,8 @@ public class ServiceDetailFragment extends Fragment { private boolean isEditing = false; private ServiceFragment serviceFragment; + @Inject ServiceApi serviceApi; + //set the service fragment to the parent so we refer back to service view when save or delete is done public void setServiceFragment(ServiceFragment fragment) { this.serviceFragment = fragment; @@ -79,8 +84,6 @@ public class ServiceDetailFragment extends Fragment { serviceDTO.setServiceDuration(duration); serviceDTO.setServicePrice(price); - ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); - //check if the service is being edited or added if (isEditing) { // Update existing service @@ -135,7 +138,6 @@ public class ServiceDetailFragment extends Fragment { .setTitle("Delete Service") .setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?") .setPositiveButton("Delete", (dialog, which) -> { - ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); serviceApi.deleteService((long) serviceId).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -205,4 +207,4 @@ public class ServiceDetailFragment extends Fragment { btnDeleteService = view.findViewById(R.id.btnDeleteService); btnBack = view.findViewById(R.id.btnBack); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index df5c5520..6125ddc5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -15,7 +15,6 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; @@ -23,10 +22,14 @@ import com.example.petstoremobile.fragments.listfragments.SupplierFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class SupplierDetailFragment extends Fragment { private TextView tvMode, tvSupId; @@ -36,6 +39,8 @@ public class SupplierDetailFragment extends Fragment { private boolean isEditing = false; private SupplierFragment supplierFragment; + @Inject SupplierApi supplierApi; + //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done public void setSupplierFragment(SupplierFragment fragment) { this.supplierFragment = fragment; @@ -82,8 +87,6 @@ public class SupplierDetailFragment extends Fragment { supplierDTO.setSupEmail(email); supplierDTO.setSupPhone(phone); - SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext()); - //check if the supplier is being edited or added if (isEditing) { // Update existing supplier @@ -138,7 +141,6 @@ public class SupplierDetailFragment extends Fragment { .setTitle("Delete Supplier") .setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?") .setPositiveButton("Delete", (dialog, which) -> { - SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext()); supplierApi.deleteSupplier((long) supId).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -215,4 +217,4 @@ public class SupplierDetailFragment extends Fragment { btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); btnBack = view.findViewById(R.id.btnBack); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index c4c47d31..bf51556a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -29,7 +29,6 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; @@ -40,6 +39,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -47,6 +50,7 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +@AndroidEntryPoint public class PetProfileFragment extends Fragment { private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; @@ -56,6 +60,9 @@ public class PetProfileFragment extends Fragment { private int petId; private boolean hasImage = false; + @Inject PetApi petApi; + @Inject @Named("baseUrl") String baseUrl; + // launchers for camera and gallery private ActivityResultLauncher galleryLauncher; private ActivityResultLauncher cameraLauncher; @@ -201,7 +208,7 @@ public class PetProfileFragment extends Fragment { // Helper function to load pet image from backend private void loadPetImage(int petId) { - String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); + String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); Glide.with(this) .load(imageUrl) @@ -236,7 +243,6 @@ public class PetProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); // Call the backend to upload the image - PetApi petApi = RetrofitClient.getPetApi(requireContext()); petApi.uploadPetImage((long) petId, body).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { @@ -261,7 +267,6 @@ public class PetProfileFragment extends Fragment { } private void deletePetImage() { - PetApi petApi = RetrofitClient.getPetApi(requireContext()); petApi.deletePetImage((long) petId).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { diff --git a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java index a1e16fa6..a502d8d0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java +++ b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java @@ -8,7 +8,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.api.CustomerApi; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.CustomerDTO; @@ -21,11 +20,17 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +import dagger.hilt.android.AndroidEntryPoint; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; // Service to receive notifications when a new conversation is created +@AndroidEntryPoint public class ChatNotificationService extends Service { private static final String TAG = "ChatNotificationService"; @@ -37,6 +42,11 @@ public class ChatNotificationService extends Service { private final Map customerIdToName = new HashMap<>(); private Long currentUserId; + @Inject CustomerApi customerApi; + @Inject ChatApi chatApi; + @Inject TokenManager tokenManager; + @Inject @Named("baseUrl") String baseUrl; + //When the service starts, connect to the websocket @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -48,14 +58,11 @@ public class ChatNotificationService extends Service { // helper function to connect to the websocket private void connectWebSocket() { //get the token and role from the shared preferences - TokenManager tm = TokenManager.getInstance(this); - String token = tm.getToken(); - String role = tm.getRole(); - currentUserId = tm.getUserId(); + String token = tokenManager.getToken(); + String role = tokenManager.getRole(); + currentUserId = tokenManager.getUserId(); if (token != null && stompChatManager == null) { - //load customers to have names associated with customer ids - CustomerApi customerApi = RetrofitClient.getCustomerApi(this); customerApi.getAllCustomers(0, 1000).enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { @@ -78,7 +85,6 @@ public class ChatNotificationService extends Service { private void loadConversationsAndStartStomp(String token, String role) { // Fetch existing conversations - ChatApi chatApi = RetrofitClient.getChatApi(this); chatApi.getAllConversations().enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { @@ -110,7 +116,7 @@ public class ChatNotificationService extends Service { private void startStomp(String token, String role) { if (stompChatManager != null) return; - stompChatManager = new StompChatManager(token, role); + stompChatManager = new StompChatManager(token, role, baseUrl); // Listen for messages in existing conversations stompChatManager.setMessageListener(message -> { @@ -193,7 +199,6 @@ public class ChatNotificationService extends Service { // Helper function to fetch customer name for a conversation private void fetchCustomerName(Long customerId) { - CustomerApi customerApi = RetrofitClient.getCustomerApi(this); customerApi.getCustomerById(customerId).enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { @@ -223,4 +228,4 @@ public class ChatNotificationService extends Service { public IBinder onBind(Intent intent) { return null; } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java index af4264db..f0f5d7ec 100644 --- a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java +++ b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java @@ -3,7 +3,6 @@ package com.example.petstoremobile.websocket; import android.os.Handler; import android.os.Looper; import android.util.Log; -import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.MessageDTO; import com.google.gson.Gson; @@ -54,14 +53,16 @@ public class StompChatManager { private ConnectionListener connectionListener; private final String authToken; private final String role; + private final String baseUrl; private boolean isConnected; private boolean isConnecting; private boolean manualDisconnect; private Long pendingConversationId; - public StompChatManager(String authToken, String role) { + public StompChatManager(String authToken, String role, String baseUrl) { this.authToken = authToken; this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT); + this.baseUrl = baseUrl; } public void setMessageListener(MessageListener listener) { @@ -267,16 +268,16 @@ public class StompChatManager { // Make the URL for the websocket connection private String buildWebSocketUrl() { - String baseUrl = RetrofitClient.BASE_URL.endsWith("/") - ? RetrofitClient.BASE_URL.substring(0, RetrofitClient.BASE_URL.length() - 1) - : RetrofitClient.BASE_URL; - if (baseUrl.startsWith("https://")) { - return "wss://" + baseUrl.substring("https://".length()) + "/ws/chat"; + String cleanBaseUrl = baseUrl.endsWith("/") + ? baseUrl.substring(0, baseUrl.length() - 1) + : baseUrl; + if (cleanBaseUrl.startsWith("https://")) { + return "wss://" + cleanBaseUrl.substring("https://".length()) + "/ws/chat"; } - if (baseUrl.startsWith("http://")) { - return "ws://" + baseUrl.substring("http://".length()) + "/ws/chat"; + if (cleanBaseUrl.startsWith("http://")) { + return "ws://" + cleanBaseUrl.substring("http://".length()) + "/ws/chat"; } - return baseUrl + "/ws/chat"; + return cleanBaseUrl + "/ws/chat"; } // Helper to check if the current user is a customer @@ -292,4 +293,4 @@ public class StompChatManager { reconnectHandler.removeCallbacksAndMessages(null); reconnectHandler.postDelayed(this::connect, 1000); } -} +} \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 37562787..91c68295 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { alias(libs.plugins.android.application) apply false + alias(libs.plugins.hilt) apply false } \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 18b8ebb8..8e1b4f14 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -8,6 +8,7 @@ material = "1.13.0" activity = "1.12.4" constraintlayout = "2.2.1" swiperefreshlayout = "1.2.0" +hilt = "2.51.1" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -18,7 +19,9 @@ material = { group = "com.google.android.material", name = "material", version.r activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } - +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } From eee724d4f53f468a80fc82bd2318e1d0aaed3639 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:23:15 -0600 Subject: [PATCH 050/137] fixed retrofit client, but will delete this file after merges - kept class so nothing will break when merge - then delete after merge buy making other files use Hilt --- .../java/com/example/petstoremobile/api/RetrofitClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index b08af532..971e7306 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -7,6 +7,7 @@ import android.util.Log; import com.example.petstoremobile.BuildConfig; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.AuthInterceptor; +import com.example.petstoremobile.api.auth.TokenManager; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; @@ -15,7 +16,7 @@ import retrofit2.converter.gson.GsonConverterFactory; import java.util.concurrent.TimeUnit; -//Retrofit client Used for API calls +//Retrofit client Used for API calls TODO: DELETE THIS FILE AFTER MERGE NOW THAT WE ARE USING HILT AND NETWORKMODULE public class RetrofitClient { private static final String TAG = "RetrofitClient"; public static final String BASE_URL = getBaseUrl(); @@ -50,7 +51,7 @@ public class RetrofitClient { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(interceptor) - .addInterceptor(new AuthInterceptor(context)) + .addInterceptor(new AuthInterceptor(new TokenManager(context))) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) From f59624f9c3f0007c3820255fc7e207f16b471f57 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:08:40 -0600 Subject: [PATCH 051/137] integrated Jetpack navigation to project so we dont have to manually code the functionallities of loading to different fragments --- android/app/build.gradle.kts | 4 + .../activities/HomeActivity.java | 62 ++---- .../petstoremobile/adapters/PetAdapter.java | 20 +- .../adapters/ProductAdapter.java | 20 +- .../fragments/ChatFragment.java | 1 + .../fragments/ListFragment.java | 125 ++++-------- .../listfragments/AdoptionFragment.java | 18 +- .../listfragments/AppointmentFragment.java | 19 +- .../listfragments/InventoryFragment.java | 26 +-- .../fragments/listfragments/PetFragment.java | 36 ++-- .../listfragments/ProductFragment.java | 24 ++- .../ProductSupplierFragment.java | 15 +- .../listfragments/PurchaseOrderFragment.java | 18 +- .../fragments/listfragments/SaleFragment.java | 22 +-- .../listfragments/ServiceFragment.java | 29 ++- .../listfragments/SupplierFragment.java | 27 +-- .../AdoptionDetailFragment.java | 7 +- .../AppointmentDetailFragment.java | 7 +- .../InventoryDetailFragment.java | 7 +- .../detailfragments/PetDetailFragment.java | 18 +- .../ProductDetailFragment.java | 24 ++- .../ProductSupplierDetailFragment.java | 7 +- .../PurchaseOrderDetailFragment.java | 5 +- .../detailfragments/RefundDetailFragment.java | 9 +- .../ServiceDetailFragment.java | 7 +- .../SupplierDetailFragment.java | 16 +- .../PetProfileFragment.java | 35 ++-- .../app/src/main/res/layout/activity_home.xml | 8 +- .../app/src/main/res/layout/fragment_list.xml | 180 +++++++----------- .../main/res/navigation/list_nav_graph.xml | 136 +++++++++++++ .../app/src/main/res/navigation/nav_graph.xml | 26 +++ android/build.gradle.kts | 1 + android/gradle/libs.versions.toml | 4 + 33 files changed, 508 insertions(+), 455 deletions(-) create mode 100644 android/app/src/main/res/navigation/list_nav_graph.xml create mode 100644 android/app/src/main/res/navigation/nav_graph.xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ef712a40..87add966 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,6 +3,7 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.hilt) + alias(libs.plugins.navigation.safeargs) } val localProperties = Properties().apply { @@ -64,6 +65,9 @@ dependencies { implementation(libs.hilt.android) annotationProcessor(libs.hilt.compiler) + implementation(libs.navigation.fragment) + implementation(libs.navigation.ui) + implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 513c3683..09bcea9d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -15,12 +15,11 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; import com.example.petstoremobile.R; -import com.example.petstoremobile.fragments.ChatFragment; -import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.ProfileFragment; import com.example.petstoremobile.services.ChatNotificationService; import com.google.android.material.bottomnavigation.BottomNavigationView; @@ -29,6 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class HomeActivity extends AppCompatActivity { private BottomNavigationView bottomNav; + private NavController navController; // Launcher to ask for notification permission private final ActivityResultLauncher requestPermissionLauncher = @@ -52,66 +52,52 @@ public class HomeActivity extends AppCompatActivity { //get the bottom navbar from the layout bottomNav = findViewById(R.id.bottom_navigation); - + + // Initialize Navigation Component + NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() + .findFragmentById(R.id.nav_host_fragment); + if (navHostFragment != null) { + navController = navHostFragment.getNavController(); + NavigationUI.setupWithNavController(bottomNav, navController); + } + //load the list fragment by default if it's a fresh start if (savedInstanceState == null) { handleIntent(getIntent()); } - //when an item in the bottom bar is selected, load the corresponding fragment - bottomNav.setOnItemSelectedListener(item -> { - if (item.getItemId() == R.id.nav_list) { - loadFragment(new ListFragment()); - return true; - } else if (item.getItemId() == R.id.nav_chat) { - loadFragment(new ChatFragment()); - return true; - } else if (item.getItemId() == R.id.nav_profile) { - loadFragment(new ProfileFragment()); - return true; - } - return false; - }); - // Start the notification service and request for notification permission startNotificationService(); requestNotificationPermission(); } - // Handle new intents when the activity is already running, - // like clicking a notification while the app is in use @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleIntent(intent); } - // Helper function to process intents for navigation. - // like clicking a notification or just launching the app from a fresh start private void handleIntent(Intent intent) { if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { - ChatFragment chatFragment = new ChatFragment(); + Bundle args = new Bundle(); if (intent.hasExtra("conversation_id")) { - Bundle args = new Bundle(); args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1)); - chatFragment.setArguments(args); } - loadFragment(chatFragment); - bottomNav.setSelectedItemId(R.id.nav_chat); - } else { - loadFragment(new ListFragment()); - bottomNav.setSelectedItemId(R.id.nav_list); + // Use NavController to navigate + if (navController != null) { + navController.navigate(R.id.nav_chat, args); + } } } - // Helper function to start the notification service in the background + // Function to start the notification service in the background // to receive notifications when a new conversation is created private void startNotificationService() { Intent serviceIntent = new Intent(this, ChatNotificationService.class); startService(serviceIntent); } - //Helper function to request for notification permission + //Function to request for notification permission private void requestNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { @@ -119,12 +105,4 @@ public class HomeActivity extends AppCompatActivity { } } } - - //Helper function to load a fragment - private void loadFragment(Fragment fragment) { - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit(); - } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index d5008aff..1f1c5120 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -11,6 +11,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; @@ -21,6 +23,7 @@ public class PetAdapter extends RecyclerView.Adapter { private List petList; private OnPetClickListener petClickListener; private String baseUrl; + private String token; // Interface for pet click on recycler view public interface OnPetClickListener { @@ -37,6 +40,10 @@ public class PetAdapter extends RecyclerView.Adapter { this.baseUrl = baseUrl; } + public void setToken(String token) { + this.token = token; + } + // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; @@ -89,11 +96,18 @@ public class PetAdapter extends RecyclerView.Adapter { // Load pet image using Glide with circle crop if (baseUrl != null) { String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); + + Object loadTarget = imageUrl; + if (token != null) { + loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + } + Glide.with(holder.itemView.getContext()) - .load(imageUrl) + .load(loadTarget) .circleCrop() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.ALL) // Changed to ALL for better performance .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .into(holder.ivPetProfile); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 5bcc4d3a..2363a682 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -8,6 +8,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.ProductDTO; @@ -18,6 +20,7 @@ public class ProductAdapter extends RecyclerView.Adapter productList; private OnProductClickListener listener; private String baseUrl; + private String token; public interface OnProductClickListener { void onProductClick(int position); @@ -32,6 +35,10 @@ public class ProductAdapter extends RecyclerView.Adapter { - loadFragment(new PetFragment()); - drawerLayout.closeDrawers(); - }); - - //Services - drawerServices.setOnClickListener(v -> { - loadFragment(new ServiceFragment()); - drawerLayout.closeDrawers(); - }); - - //Suppliers - drawerSuppliers.setOnClickListener(v -> { - loadFragment(new SupplierFragment()); - drawerLayout.closeDrawers(); - }); - - //Adoptions - - drawerAdoptions.setOnClickListener(v -> { - loadFragment(new AdoptionFragment()); - drawerLayout.closeDrawers(); - }); - - //Appointment - drawerAppointments.setOnClickListener(v -> { - loadFragment(new AppointmentFragment()); - drawerLayout.closeDrawers(); - }); - - //Inventory - drawerInventory.setOnClickListener(v -> { - loadFragment(new InventoryFragment()); - drawerLayout.closeDrawers(); - }); - - //Products - drawerProducts.setOnClickListener(v -> { - loadFragment(new ProductFragment()); - drawerLayout.closeDrawers(); - }); - - //ProductSupplier - - drawerProductSupplier.setOnClickListener(v -> { - loadFragment(new ProductSupplierFragment()); - drawerLayout.closeDrawers(); - }); - - //Purchase - - drawerPurchaseOrderView.setOnClickListener(v -> { - loadFragment(new PurchaseOrderFragment()); - drawerLayout.closeDrawers(); - }); - - //Sale - - drawerSale.setOnClickListener(v -> { - loadFragment(new SaleFragment()); - drawerLayout.closeDrawers(); - }); + drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet)); + drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service)); + drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier)); + drawerAdoptions.setOnClickListener(v -> navigateTo(R.id.nav_adoption)); + drawerAppointments.setOnClickListener(v -> navigateTo(R.id.nav_appointment)); + drawerInventory.setOnClickListener(v -> navigateTo(R.id.nav_inventory)); + drawerProducts.setOnClickListener(v -> navigateTo(R.id.nav_product)); + drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier)); + drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); + drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); return view; } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager() + .findFragmentById(R.id.inner_nav_host_fragment); + if (navHostFragment != null) { + innerNavController = navHostFragment.getNavController(); + } + } + + private void navigateTo(int destinationId) { + if (innerNavController != null) { + innerNavController.navigate(destinationId); + } + drawerLayout.closeDrawers(); + } + //helper function to open the drawer public void openDrawer() { drawerLayout.openDrawer(GravityCompat.START); } - - // helper function to load the fragment into the display - public void loadFragment(Fragment fragment) { - getChildFragmentManager() - .beginTransaction() - .replace(R.id.inner_fragment_container, fragment) - .addToBackStack(null) - .commit(); - } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 06757a32..92c8cdea 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -8,6 +8,7 @@ import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -17,7 +18,6 @@ import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment; import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; @@ -68,8 +68,13 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop fab.setOnClickListener(v -> openDetail(-1)); hamburger.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); @@ -197,7 +202,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } private void openDetail(int position) { - AdoptionDetailFragment detail = new AdoptionDetailFragment(); Bundle args = new Bundle(); if (position != -1) { @@ -209,11 +213,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop args.putString("adoptionStatus", a.getAdoptionStatus()); } - detail.setArguments(args); - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.loadFragment(detail); + NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); } @Override public void onAdoptionClick(int position) { openDetail(position); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 83115a3b..3f50f7cc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -28,7 +29,6 @@ import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; @@ -96,9 +96,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. fabAdd.setOnClickListener(v -> openAppointmentDetails(-1)); hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) - listFragment.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); @@ -205,7 +209,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } private void openAppointmentDetails(int position) { - AppointmentDetailFragment detailFragment = new AppointmentDetailFragment(); Bundle args = new Bundle(); if (position != -1) { @@ -221,9 +224,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId()); } - detailFragment.setArguments(args); - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.loadFragment(detailFragment); + NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } public void onAppointmentSaved(int position, AppointmentDTO appointment) { loadAppointmentData(); @@ -333,4 +334,4 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index ce93ca54..0bfe389b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -17,7 +17,10 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -32,8 +35,6 @@ import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; @@ -100,9 +101,13 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn .setOnClickListener(v -> openDetail(null)); hamburger.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) - lf.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); @@ -174,7 +179,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchInventory); etSearch.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + @Override + public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } @Override @@ -332,7 +338,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn // Navigation private void openDetail(InventoryDTO inv) { - InventoryDetailFragment detail = new InventoryDetailFragment(); Bundle args = new Bundle(); if (inv != null) { @@ -343,12 +348,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0); } - detail.setArguments(args); - detail.setInventoryFragment(this); - - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) - lf.loadFragment(detail); + NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } public void onInventoryChanged() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 693e261f..ac456828 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -2,7 +2,10 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -26,8 +29,6 @@ import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; -import com.example.petstoremobile.fragments.listfragments.listprofilefragments.PetProfileFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; @@ -73,10 +74,12 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } } }); @@ -150,8 +153,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen //Open pet profile private void openPetProfile(int position) { - PetProfileFragment profileFragment = new PetProfileFragment(); - //Make a bundle to pass data to the profile fragment Bundle args = new Bundle(); PetDTO pet = filteredList.get(position); @@ -168,25 +169,12 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen args.putDouble("petPrice", 0.0); } - //send the bundle to the profile fragment to display - profileFragment.setArguments(args); - - //get ListFragment to load the the pet profile view - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.loadFragment(profileFragment); - } + NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } //Open the pet detail view for adding private void openPetDetails(int position) { - PetDetailFragment detailFragment = new PetDetailFragment(); - - //get ListFragment to load the detail view - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.loadFragment(detailFragment); - } + NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } // Called by PetAdapter when a row is clicked to open the details view @@ -236,4 +224,4 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index ce15a7e8..16d6347f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -6,20 +6,22 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; import javax.inject.Inject; +import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; @@ -34,6 +36,8 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private EditText etSearch; @Inject ProductApi api; + @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -51,8 +55,13 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc ImageButton hamburger = view.findViewById(R.id.btnHamburgerProduct); hamburger.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); return view; @@ -61,6 +70,8 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewProducts); adapter = new ProductAdapter(filteredList, this); + adapter.setBaseUrl(baseUrl); + adapter.setToken(tokenManager.getToken()); rv.setLayoutManager(new LinearLayoutManager(getContext())); rv.setAdapter(adapter); } @@ -122,7 +133,6 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } private void openDetail(int position) { - ProductDetailFragment detail = new ProductDetailFragment(); Bundle args = new Bundle(); if (position != -1) { ProductDTO p = filteredList.get(position); @@ -132,11 +142,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : ""); args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1); } - detail.setArguments(args); - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.loadFragment(detail); + NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } @Override public void onProductClick(int position) { openDetail(position); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 0f76041e..18d97c9d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -51,8 +52,13 @@ public class ProductSupplierFragment extends Fragment ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS); hamburger.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); return view; @@ -121,7 +127,6 @@ public class ProductSupplierFragment extends Fragment } private void openDetail(int position) { - ProductSupplierDetailFragment detail = new ProductSupplierDetailFragment(); Bundle args = new Bundle(); if (position != -1) { ProductSupplierDTO ps = filteredList.get(position); @@ -131,9 +136,7 @@ public class ProductSupplierFragment extends Fragment args.putString("supplierName", ps.getSupplierName()); args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : ""); } - detail.setArguments(args); - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.loadFragment(detail); + NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 4840b549..9a7226a8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -15,7 +16,6 @@ import com.example.petstoremobile.api.PurchaseOrderApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.PurchaseOrderDetailFragment; import java.util.*; import javax.inject.Inject; @@ -47,9 +47,13 @@ public class PurchaseOrderFragment extends Fragment ImageButton hamburger = view.findViewById(R.id.btnHamburgerPO); hamburger.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) - lf.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } + } }); return view; @@ -126,17 +130,13 @@ public class PurchaseOrderFragment extends Fragment } private void openDetail(int position) { - PurchaseOrderDetailFragment detail = new PurchaseOrderDetailFragment(); Bundle args = new Bundle(); PurchaseOrderDTO po = filteredList.get(position); args.putLong("purchaseOrderId", po.getPurchaseOrderId()); args.putString("supplierName", po.getSupplierName()); args.putString("orderDate", po.getOrderDate()); args.putString("status", po.getStatus()); - detail.setArguments(args); - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) - lf.loadFragment(detail); + NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 74b8c4ef..9ea74d19 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -2,6 +2,7 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -17,7 +18,6 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.api.SaleApi; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundDetailFragment; import com.example.petstoremobile.models.Sale; import java.util.ArrayList; import java.util.List; @@ -53,9 +53,12 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis // Make the hamburger button open the drawer from listFragment if (btnHamburger != null) { btnHamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } } }); } @@ -111,19 +114,14 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis @Override public void onSaleClick(int position) { Sale sale = filteredList.get(position); - RefundDetailFragment refundFragment = new RefundDetailFragment(); Bundle args = new Bundle(); args.putInt("saleId", sale.getSaleId()); args.putString("saleDate", sale.getSaleDate()); args.putString("employeeName", sale.getEmployeeName()); args.putDouble("total", sale.getTotal()); args.putString("paymentMethod", sale.getPaymentMethod()); - refundFragment.setArguments(args); - refundFragment.setSaleFragment(this); - - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) - listFragment.loadFragment(refundFragment); + + NavHostFragment.findNavController(this).navigate(R.id.nav_refund_detail, args); } public void reloadSales() { @@ -149,4 +147,4 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index afa793de..dc9ef0fe 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -2,7 +2,10 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -23,7 +26,6 @@ import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.ServiceDetailFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; @@ -66,10 +68,12 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } } }); @@ -112,8 +116,6 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic //Open the service detail view depending on the mode private void openServiceDetails(int position) { - ServiceDetailFragment detailFragment = new ServiceDetailFragment(); - //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); args.putInt("position", position); @@ -128,16 +130,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic args.putDouble("servicePrice", service.getServicePrice()); } - //send the bundle to the detail fragment to display - detailFragment.setArguments(args); - //set the service fragment to the parent so we refer back to service view when save or delete is done - detailFragment.setServiceFragment(this); - - //get ListFragment to load the the detail view - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.loadFragment(detailFragment); - } + NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); } // Called by ServiceAdapter when a row is clicked to open the details view @@ -188,4 +181,4 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 66b72b8b..f9de9227 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -2,7 +2,10 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -23,7 +26,6 @@ import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.SupplierDetailFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; @@ -66,10 +68,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { - ListFragment listFragment = (ListFragment) getParentFragment(); - //if list fragment is found then use its helper function to open the drawer - if (listFragment != null) { - listFragment.openDrawer(); + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); + } } }); @@ -113,8 +117,6 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp //Open the supplier detail view depending on the mode private void openSupplierDetails(int position) { - SupplierDetailFragment detailFragment = new SupplierDetailFragment(); - //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); args.putInt("position", position); @@ -130,16 +132,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp args.putString("supPhone", supplier.getSupPhone()); } - //send the bundle to the detail fragment to display - detailFragment.setArguments(args); - //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done - detailFragment.setSupplierFragment(this); - - //get ListFragment to load the the detail view - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.loadFragment(detailFragment); - } + NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 512427d5..a9167678 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -8,6 +8,8 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; @@ -259,7 +261,6 @@ public class AdoptionDetailFragment extends Fragment { } private void navigateBack() { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 51e37666..45ffe85a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -8,6 +8,8 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; @@ -447,7 +449,6 @@ public class AppointmentDetailFragment extends Fragment { } private void navigateBack() { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index c1bb052d..92fecd47 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -16,6 +16,7 @@ import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; @@ -327,9 +328,7 @@ public class InventoryDetailFragment extends Fragment { } private void navigateBack() { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) - lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } private void setButtonsEnabled(boolean enabled) { @@ -337,4 +336,4 @@ public class InventoryDetailFragment extends Fragment { btnDelete.setEnabled(enabled); btnBack.setEnabled(enabled); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 7f9a44f1..8aa3b319 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import android.util.Log; import android.view.LayoutInflater; @@ -44,15 +45,9 @@ public class PetDetailFragment extends Fragment { private Button btnSavePet, btnDeletePet, btnBack; private int petId; private boolean isEditing = false; - private PetFragment petFragment; @Inject PetApi petApi; - //set the pet fragment to the parent so we refer back to pet view when save or delete is done - public void setPetFragment(PetFragment fragment) { - this.petFragment = fragment; - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -178,14 +173,7 @@ public class PetDetailFragment extends Fragment { //Helper method to navigate back to the list private void navigateBack() { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - // If editing pop back twice to get back to PetDetail Fragment instead of PetProfileFragment - if (isEditing) { - listFragment.getChildFragmentManager().popBackStack(); - } - listFragment.getChildFragmentManager().popBackStack(); - } + NavHostFragment.findNavController(this).popBackStack(); } //helper function to check if pet is being edited or added and show the view accordingly @@ -244,4 +232,4 @@ public class PetDetailFragment extends Fragment { spinnerPetStatus.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 228f2c01..bf13d685 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -17,13 +17,17 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.fragments.ListFragment; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; @@ -59,6 +63,7 @@ public class ProductDetailFragment extends Fragment { @Inject ProductApi productApi; @Inject CategoryApi categoryApi; @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; private ActivityResultLauncher galleryLauncher; private ActivityResultLauncher cameraLauncher; @@ -254,10 +259,18 @@ public class ProductDetailFragment extends Fragment { //load the product image from the backend private void loadProductImage() { String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + String token = tokenManager.getToken(); + + Object loadTarget = imageUrl; + if (token != null) { + loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + } + Glide.with(this) - .load(imageUrl) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) + .load(loadTarget) + .diskCacheStrategy(DiskCacheStrategy.ALL) .placeholder(R.drawable.placeholder2) .error(R.drawable.placeholder2) .into(ivProductImage); @@ -398,7 +411,6 @@ public class ProductDetailFragment extends Fragment { } private void navigateBack() { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 909df42c..d934f64f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -7,6 +7,8 @@ import android.widget.*; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; @@ -222,7 +224,6 @@ public class ProductSupplierDetailFragment extends Fragment { } private void navigateBack() { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index eec648e2..38da2aa4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -6,6 +6,8 @@ import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import com.example.petstoremobile.R; import com.example.petstoremobile.fragments.ListFragment; @@ -49,8 +51,7 @@ public class PurchaseOrderDetailFragment extends Fragment { } btnBack.setOnClickListener(v -> { - ListFragment lf = (ListFragment) getParentFragment(); - if (lf != null) lf.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); }); return view; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index 190c89c5..ce782a04 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -2,6 +2,8 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,7 +16,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.SaleApi; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; @@ -119,9 +120,7 @@ public class RefundDetailFragment extends Fragment { } private void goBack() { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) - listFragment.getChildFragmentManager().popBackStack(); + NavHostFragment.findNavController(this).popBackStack(); } private void initViews(View view) { @@ -141,4 +140,4 @@ public class RefundDetailFragment extends Fragment { adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerRefundPayment.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 4176fa38..fc65a1e1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -4,6 +4,7 @@ import android.os.Bundle; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import android.util.Log; import android.view.LayoutInflater; @@ -17,7 +18,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.ServiceFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; @@ -164,10 +164,7 @@ public class ServiceDetailFragment extends Fragment { //Helper method to navigate back to the list private void navigateBack() { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.getChildFragmentManager().popBackStack(); - } + NavHostFragment.findNavController(this).popBackStack(); } //helper function to check if service is being edited or added and show the view accordingly diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 6125ddc5..e1651c1a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -4,6 +4,7 @@ import android.os.Bundle; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import android.util.Log; import android.view.LayoutInflater; @@ -17,8 +18,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.SupplierDTO; -import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.SupplierFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; @@ -37,15 +36,9 @@ public class SupplierDetailFragment extends Fragment { private Button btnSaveSupplier, btnDeleteSupplier, btnBack; private int supId; private boolean isEditing = false; - private SupplierFragment supplierFragment; @Inject SupplierApi supplierApi; - //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done - public void setSupplierFragment(SupplierFragment fragment) { - this.supplierFragment = fragment; - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -167,10 +160,7 @@ public class SupplierDetailFragment extends Fragment { //Helper method to navigate back to the list private void navigateBack() { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.getChildFragmentManager().popBackStack(); - } + NavHostFragment.findNavController(this).popBackStack(); } //helper function to check if supplier is being edited or added and show the view accordingly @@ -217,4 +207,4 @@ public class SupplierDetailFragment extends Fragment { btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); btnBack = view.findViewById(R.id.btnBack); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index bf51556a..e8accaaf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -14,6 +14,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import androidx.navigation.fragment.NavHostFragment; import android.provider.MediaStore; import android.util.Log; @@ -27,8 +28,11 @@ import android.widget.Toast; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; @@ -62,6 +66,7 @@ public class PetProfileFragment extends Fragment { @Inject PetApi petApi; @Inject @Named("baseUrl") String baseUrl; + @Inject TokenManager tokenManager; // launchers for camera and gallery private ActivityResultLauncher galleryLauncher; @@ -148,26 +153,13 @@ public class PetProfileFragment extends Fragment { //set button click listeners btnBack.setOnClickListener(v -> { - //get the list fragment and pop the back stack to return to the previous view (PetFragment) - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.getChildFragmentManager().popBackStack(); - } + NavHostFragment.findNavController(this).popBackStack(); }); //Make the edit button go to the pet detail view btnEditPet.setOnClickListener(v -> { if (getArguments() == null) return; - - PetDetailFragment detailFragment = new PetDetailFragment(); - //send the bundle to the pet detail fragment - detailFragment.setArguments(getArguments()); - - //get ListFragment to load the the detail view - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) { - listFragment.loadFragment(detailFragment); - } + NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, getArguments()); }); //Make change photo button ask user to select a new photo @@ -209,11 +201,18 @@ public class PetProfileFragment extends Fragment { // Helper function to load pet image from backend private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); + String token = tokenManager.getToken(); + + Object loadTarget = imageUrl; + if (token != null) { + loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + } Glide.with(this) - .load(imageUrl) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) + .load(loadTarget) + .diskCacheStrategy(DiskCacheStrategy.ALL) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .listener(new com.bumptech.glide.request.RequestListener() { diff --git a/android/app/src/main/res/layout/activity_home.xml b/android/app/src/main/res/layout/activity_home.xml index 9c61c329..f5e822e2 100644 --- a/android/app/src/main/res/layout/activity_home.xml +++ b/android/app/src/main/res/layout/activity_home.xml @@ -7,12 +7,14 @@ android:orientation="vertical" android:background="@color/primary_dark"> - + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> @@ -10,10 +11,13 @@ android:layout_height="match_parent" android:background="@color/background_grey"> - + android:layout_height="match_parent" + app:defaultNavHost="false" + app:navGraph="@navigation/list_nav_graph" /> - - - - - - - - - - - - - - - - - - + android:layout_height="48dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:background="?attr/selectableItemBackground"> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + android:layout_height="wrap_content" + android:text="Sale" + android:textColor="@color/white" + android:textSize="15sp"/> + + + + + + - - - - + \ No newline at end of file diff --git a/android/app/src/main/res/navigation/list_nav_graph.xml b/android/app/src/main/res/navigation/list_nav_graph.xml new file mode 100644 index 00000000..7273dd5f --- /dev/null +++ b/android/app/src/main/res/navigation/list_nav_graph.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/navigation/nav_graph.xml b/android/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..d104e4d5 --- /dev/null +++ b/android/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 91c68295..3898c951 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.hilt) apply false + alias(libs.plugins.navigation.safeargs) apply false } \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 8e1b4f14..c7a01abf 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -9,6 +9,7 @@ activity = "1.12.4" constraintlayout = "2.2.1" swiperefreshlayout = "1.2.0" hilt = "2.51.1" +navigation = "2.8.8" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -21,7 +22,10 @@ constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayo swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" } +navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "navigation" } From 44877cd4ad42065a1cc8001ea2439c4783b94220 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:21:25 -0600 Subject: [PATCH 052/137] fix photo loading issue on pets and products --- .../petstoremobile/adapters/PetAdapter.java | 3 +- .../adapters/ProductAdapter.java | 3 +- .../ProductDetailFragment.java | 153 ++++++++++-------- .../PetProfileFragment.java | 5 +- .../app/src/main/res/layout/fragment_list.xml | 4 +- 5 files changed, 92 insertions(+), 76 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 1f1c5120..bec941f0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -107,7 +107,8 @@ public class PetAdapter extends RecyclerView.Adapter { Glide.with(holder.itemView.getContext()) .load(loadTarget) .circleCrop() - .diskCacheStrategy(DiskCacheStrategy.ALL) // Changed to ALL for better performance + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .into(holder.ivPetProfile); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 2363a682..4e7c5fea 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -83,7 +83,8 @@ public class ProductAdapter extends RecyclerView.Adapter categoryList = new ArrayList<>(); private Uri photoUri; @@ -77,13 +79,12 @@ public class ProductDetailFragment extends Fragment { result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri selectedImage = result.getData().getData(); - if (isEditing) { - uploadProductImage(selectedImage); - } else { - ivProductImage.setImageURI(selectedImage); - photoUri = selectedImage; - hasImage = true; - } + // Update image view locally + Glide.with(this).load(selectedImage).into(ivProductImage); + photoUri = selectedImage; + hasImage = true; + isImageChanged = true; + isImageRemoved = false; } } ); @@ -91,12 +92,11 @@ public class ProductDetailFragment extends Fragment { new ActivityResultContracts.TakePicture(), success -> { if (success) { - if (isEditing) { - uploadProductImage(photoUri); - } else { - ivProductImage.setImageURI(photoUri); - hasImage = true; - } + // Update image view locally + Glide.with(this).load(photoUri).into(ivProductImage); + hasImage = true; + isImageChanged = true; + isImageRemoved = false; } } ); @@ -167,31 +167,13 @@ public class ProductDetailFragment extends Fragment { .show(); } - // Helper function to remove the photo + // Helper function to remove the photo locally private void removePhoto() { - if (isEditing) { - productApi.deleteProductImage(prodId) - .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(getContext(), "Photo removed", Toast.LENGTH_SHORT).show(); - ivProductImage.setImageResource(R.drawable.placeholder2); - hasImage = false; - } else { - Toast.makeText(getContext(), "Failed to remove photo", Toast.LENGTH_SHORT).show(); - } - } - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); - } else { - photoUri = null; - hasImage = false; - ivProductImage.setImageResource(R.drawable.placeholder2); - } + photoUri = null; + hasImage = false; + isImageChanged = false; + isImageRemoved = true; + Glide.with(this).load(R.drawable.placeholder2).into(ivProductImage); } // Helper function to launch the camera @@ -201,6 +183,7 @@ public class ProductDetailFragment extends Fragment { cameraLauncher.launch(photoUri); } + // Helper function to load categories from the backend for the spinner private void loadCategories() { categoryApi.getAllCategories(0, 100) .enqueue(new Callback>() { @@ -217,6 +200,7 @@ public class ProductDetailFragment extends Fragment { }); } + // Helper function to populate the category spinner private void populateCategorySpinner() { List names = new ArrayList<>(); names.add("-- Select Category --"); @@ -270,17 +254,50 @@ public class ProductDetailFragment extends Fragment { Glide.with(this) .load(loadTarget) - .diskCacheStrategy(DiskCacheStrategy.ALL) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) .placeholder(R.drawable.placeholder2) .error(R.drawable.placeholder2) .into(ivProductImage); } - // Function to upload the product image by calling the backend - private void uploadProductImage(Uri uri) { + // Function to check any changes to the image and perform the appropriate action + // updating/adding photo, removing photo or no change + private void performPendingImageActions(String successMsg) { + if (isImageRemoved) { + //if the image is removed then delete the image + productApi.deleteProductImage(prodId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); + navigateBack(); + } + }); + } else if (isImageChanged && photoUri != null) { + //if the image is changed then upload it + uploadProductImageAndNavigate(photoUri, successMsg); + } else { + //if no changes then navigate back + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateBack(); + } + } + + // Helper function to upload the product image by calling the backend + // and then navigate back to the previous screen + private void uploadProductImageAndNavigate(Uri uri, String successMsg) { try { File file = getFileFromUri(uri); - if (file == null) return; + if (file == null) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateBack(); + return; + } RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); @@ -290,20 +307,22 @@ public class ProductDetailFragment extends Fragment { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { - Toast.makeText(getContext(), "Image uploaded", Toast.LENGTH_SHORT).show(); - hasImage = true; - loadProductImage(); + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { - Toast.makeText(getContext(), "Upload failed", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); } + navigateBack(); } @Override public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), successMsg + " (network error during upload)", Toast.LENGTH_SHORT).show(); + navigateBack(); } }); } catch (Exception e) { Log.e("ProductDetail", "Error uploading image", e); + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateBack(); } } @@ -326,6 +345,7 @@ public class ProductDetailFragment extends Fragment { } } + // Function to save the product to the server private void saveProduct() { String name = etProductName.getText().toString().trim(); String desc = etProductDesc.getText().toString().trim(); @@ -352,19 +372,27 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); if (isEditing) { - productApi.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); + productApi.updateProduct(prodId, dto).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + performPendingImageActions("Updated"); + } else { + Toast.makeText(getContext(), "Error " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); } else { productApi.createProduct(dto).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null) { - long newId = response.body().getProdId(); - if (photoUri != null) { - prodId = newId; - uploadProductImage(photoUri); - } - Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); - navigateBack(); + prodId = response.body().getProdId(); + performPendingImageActions("Saved"); } else { Toast.makeText(getContext(), "Error saving", Toast.LENGTH_SHORT).show(); } @@ -377,22 +405,7 @@ public class ProductDetailFragment extends Fragment { } } - private Callback simpleCallback(String msg) { - return new Callback<>() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful()) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); - } - } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }; - } - + // Function to delete the product from the server private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete Product?") diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index e8accaaf..6e6943d1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -212,7 +212,8 @@ public class PetProfileFragment extends Fragment { Glide.with(this) .load(loadTarget) - .diskCacheStrategy(DiskCacheStrategy.ALL) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) .listener(new com.bumptech.glide.request.RequestListener() { @@ -311,4 +312,4 @@ public class PetProfileFragment extends Fragment { photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); cameraLauncher.launch(photoUri); } -} \ No newline at end of file +} diff --git a/android/app/src/main/res/layout/fragment_list.xml b/android/app/src/main/res/layout/fragment_list.xml index 58d0bb2f..a55933b9 100644 --- a/android/app/src/main/res/layout/fragment_list.xml +++ b/android/app/src/main/res/layout/fragment_list.xml @@ -16,7 +16,7 @@ android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" - app:defaultNavHost="false" + app:defaultNavHost="true" app:navGraph="@navigation/list_nav_graph" /> - \ No newline at end of file + From 453cb54f19dadc4f5d89e96a8869f3924205dd5d Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:35:38 -0600 Subject: [PATCH 053/137] Refactored Andriod project to use MVVM structure (Need to apply this so sales too after merge) - Used MVVM structure so fragments are not doing all the operation from views to data and calls - organized the structure of our proejct --- .../activities/MainActivity.java | 148 +++++-------- .../listfragments/AdoptionFragment.java | 65 +++--- .../listfragments/AppointmentFragment.java | 122 +++++----- .../listfragments/InventoryFragment.java | 194 ++++++++-------- .../fragments/listfragments/PetFragment.java | 84 ++++--- .../listfragments/ProductFragment.java | 64 ++++-- .../ProductSupplierFragment.java | 70 +++--- .../listfragments/PurchaseOrderFragment.java | 69 +++--- .../fragments/listfragments/SaleFragment.java | 2 + .../listfragments/ServiceFragment.java | 82 ++++--- .../listfragments/SupplierFragment.java | 86 ++++--- .../ProductDetailFragment.java | 164 +++++--------- .../petstoremobile/models/Adoption.java | 79 ------- .../petstoremobile/models/Appointment.java | 76 ------- .../petstoremobile/models/Inventory.java | 66 ------ .../petstoremobile/models/Product.java | 66 ------ .../models/ProductSupplier.java | 49 ---- .../petstoremobile/models/PurchaseOrder.java | 31 --- .../repositories/AdoptionRepository.java | 156 +++++++++++++ .../repositories/AppointmentRepository.java | 156 +++++++++++++ .../repositories/AuthRepository.java | 174 +++++++++++++++ .../repositories/CategoryRepository.java | 52 +++++ .../repositories/InventoryRepository.java | 184 +++++++++++++++ .../repositories/PetRepository.java | 208 +++++++++++++++++ .../repositories/ProductRepository.java | 209 ++++++++++++++++++ .../ProductSupplierRepository.java | 130 +++++++++++ .../repositories/PurchaseOrderRepository.java | 78 +++++++ .../repositories/ServiceRepository.java | 156 +++++++++++++ .../repositories/SupplierRepository.java | 156 +++++++++++++ .../petstoremobile/utils/FileUtils.java | 27 +++ .../petstoremobile/utils/Resource.java | 30 +++ .../viewmodels/AdoptionViewModel.java | 58 +++++ .../viewmodels/AppointmentViewModel.java | 58 +++++ .../viewmodels/AuthViewModel.java | 68 ++++++ .../viewmodels/InventoryViewModel.java | 80 +++++++ .../viewmodels/PetViewModel.java | 75 +++++++ .../viewmodels/ProductSupplierViewModel.java | 51 +++++ .../viewmodels/ProductViewModel.java | 84 +++++++ .../viewmodels/PurchaseOrderViewModel.java | 37 ++++ .../viewmodels/ServiceViewModel.java | 58 +++++ .../viewmodels/SupplierViewModel.java | 58 +++++ 41 files changed, 2869 insertions(+), 991 deletions(-) delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/Adoption.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/Appointment.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/Inventory.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/Product.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/models/PurchaseOrder.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/Resource.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index b4b00db4..d090a09e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -14,20 +14,17 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.ViewModelProvider; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.dtos.AuthDTO; -import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.viewmodels.AuthViewModel; +import com.example.petstoremobile.utils.Resource; import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; //The login screen activity @AndroidEntryPoint @@ -37,12 +34,11 @@ public class MainActivity extends AppCompatActivity { private EditText etPassword; private Button btnLogin; private TextView tvLoginStatus; + private AuthViewModel viewModel; - @Inject AuthApi authApi; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; - @Override protected void onCreate(Bundle savedInstanceState) { EdgeToEdge.enable(this); @@ -54,8 +50,7 @@ public class MainActivity extends AppCompatActivity { // If a customer somehow remained logged in, clear them out tokenManager.clearLoginData(); } else { - Intent intent = new Intent(this, HomeActivity.class); - startActivity(intent); + startActivity(new Intent(this, HomeActivity.class)); finish(); return; } @@ -99,92 +94,51 @@ public class MainActivity extends AppCompatActivity { return; } - //Call login from api and get response - authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - String role = response.body().getRole(); - - // Check if the user is a CUSTOMER and deny login if so - if ("CUSTOMER".equalsIgnoreCase(role)) { - Toast.makeText(MainActivity.this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show(); - tvLoginStatus.setText("Customers are not allowed to log in"); - return; - } - - //save login data in shared preferences - tokenManager.saveLoginData( - response.body().getToken(), - response.body().getUsername(), - role - ); - - //fetch user id from api then login to home activity - authApi.getMe() - .enqueue(new Callback() { - @Override - public void onResponse(Call call, - Response response) { - if (response.isSuccessful() && response.body() != null) { - tokenManager.saveUserId(response.body().getId()); - } - - Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); - startActivity(new Intent(MainActivity.this, HomeActivity.class)); - finish(); - } - - @Override - public void onFailure(Call call, - Throwable t) { - Log.e("MainActivity", "Failed to fetch userId", t); - - Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); - startActivity(new Intent(MainActivity.this, HomeActivity.class)); - finish(); - } - }); - } else { - String errorMessage; - switch (response.code()) { - case 401: - errorMessage = "Invalid username or password"; - break; - case 500: - errorMessage = "Server error. Please try again later."; - break; - case 503: - errorMessage = "Service unavailable. Backend may be starting up."; - break; - default: - errorMessage = "Login failed (Error " + response.code() + ")"; - } - Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show(); - tvLoginStatus.setText(errorMessage); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("MainActivity", "Login request failed", t); - - String errorMessage; - if (t instanceof java.net.ConnectException || - t instanceof java.net.SocketTimeoutException || - t instanceof java.net.UnknownHostException) { - errorMessage = "Cannot connect to server at " + baseUrl + - ". Please check if the backend is running."; - } else if (t instanceof java.io.IOException) { - errorMessage = "Network error. Please check your connection."; - } else { - errorMessage = "Login failed: " + t.getMessage(); - } - - Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show(); - tvLoginStatus.setText(errorMessage); - } - }); + performLogin(username, password); }); } -} \ No newline at end of file + + private void performLogin(String username, String password) { + viewModel.login(username, password).observe(this, resource -> { + if (resource == null) return; + + switch (resource.status) { + case LOADING: + btnLogin.setEnabled(false); + tvLoginStatus.setText("Logging in..."); + break; + case SUCCESS: + if (resource.data != null) { + String role = resource.data.getRole(); + if ("CUSTOMER".equalsIgnoreCase(role)) { + btnLogin.setEnabled(true); + tvLoginStatus.setText("Customers are not allowed to log in"); + Toast.makeText(this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show(); + } else { + tokenManager.saveLoginData(resource.data.getToken(), resource.data.getUsername(), role); + fetchUserIdAndNavigate(); + } + } + break; + case ERROR: + btnLogin.setEnabled(true); + tvLoginStatus.setText(resource.message); + Toast.makeText(this, resource.message, Toast.LENGTH_LONG).show(); + break; + } + }); + } + + private void fetchUserIdAndNavigate() { + viewModel.getMe().observe(this, resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + tokenManager.saveUserId(resource.data.getId()); + } + Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(this, HomeActivity.class)); + finish(); + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 92c8cdea..69bcee15 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -7,17 +7,19 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; -import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.AdoptionViewModel; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; @@ -28,10 +30,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; @AndroidEntryPoint public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { @@ -39,7 +38,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private List adoptionList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private AdoptionAdapter adapter; - @Inject AdoptionApi api; + private AdoptionViewModel viewModel; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; private ImageButton hamburger; @@ -50,7 +49,13 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_adoption, container, false); @@ -177,26 +182,34 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adapter.notifyDataSetChanged(); } + // Helper function to get a list of all adoptions from the backend private void loadAdoptions() { - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - api.getAllAdoptions(0, 500).enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - if (r.isSuccessful() && r.body() != null) { - adoptionList.clear(); - adoptionList.addAll(r.body().getContent()); - updateCalendarDecorators(); - filter(etSearch != null ? etSearch.getText().toString() : ""); - } else { - Toast.makeText(getContext(), "Failed to load adoptions", Toast.LENGTH_SHORT).show(); - Log.e("AdoptionFragment", "Error: " + r.message()); - } - } - public void onFailure(Call> c, Throwable t) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - Log.e("AdoptionFragment", t.getMessage()); + //Load all adoptions from the backend using viewModel + viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (resource.data != null) { + adoptionList.clear(); + adoptionList.addAll(resource.data.getContent()); + updateCalendarDecorators(); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message); + break; } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index 3f50f7cc..b284a2bf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -4,7 +4,9 @@ import android.graphics.Color; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -21,14 +23,14 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; -import com.example.petstoremobile.api.AppointmentApi; -import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.AppointmentViewModel; +import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; @@ -45,12 +47,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { @@ -61,9 +58,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private List serviceList = new ArrayList<>(); private AppointmentAdapter adapter; - @Inject AppointmentApi api; - @Inject PetApi petApi; - @Inject ServiceApi serviceApi; + private AppointmentViewModel appointmentViewModel; + private PetViewModel petViewModel; + private ServiceViewModel serviceViewModel; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; @@ -75,7 +72,15 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); + petViewModel = new ViewModelProvider(this).get(PetViewModel.class); + serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_appointment, container, false); @@ -91,7 +96,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. loadPets(); loadServices(); - FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment); fabAdd.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -239,76 +243,54 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. openAppointmentDetails(position); } + // Helper function to get a list of all appointments from the backend private void loadAppointmentData() { - if (swipeRefreshLayout != null) - swipeRefreshLayout.setRefreshing(true); - api.getAllAppointments(0, 500).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, - Response> response) { - if (swipeRefreshLayout != null) - swipeRefreshLayout.setRefreshing(false); - if (response.isSuccessful() && response.body() != null) { - appointmentList.clear(); - appointmentList.addAll(response.body().getContent()); - updateCalendarDecorators(); - filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); - } else { - Log.e("AppointmentFragment", "Error: " + response.message()); - Toast.makeText(getContext(), "Failed to load appointments", Toast.LENGTH_SHORT).show(); - } - } + //Load all appointments from the backend using viewModel + appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; - @Override - public void onFailure(Call> call, Throwable t) { - if (swipeRefreshLayout != null) - swipeRefreshLayout.setRefreshing(false); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - Log.e("AppointmentFragment", t.getMessage()); + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (resource.data != null) { + appointmentList.clear(); + appointmentList.addAll(resource.data.getContent()); + updateCalendarDecorators(); + filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); + } + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("AppointmentFragment", "Error loading appointments: " + resource.message); + break; } }); } - - // Load Pets private void loadPets() { - petApi.getAllPets(0,100).enqueue(new Callback>() { - - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() !=null) { - petList.clear(); - petList.addAll(response.body().getContent()); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - - Log.e("AppointmentFragment", "Pet load error:" + t.getMessage()); - + petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + petList.clear(); + petList.addAll(resource.data.getContent()); } }); } // Load Services - private void loadServices() { - serviceApi.getAllServices(0,100).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - serviceList.clear(); - serviceList.addAll(response.body().getContent()); - - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - Log.e("AppointmentFragmnet", "Service load error: " + t.getMessage()); - + serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + serviceList.clear(); + serviceList.addAll(resource.data.getContent()); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 0bfe389b..e77ae958 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -20,6 +20,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -28,23 +29,16 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; -import com.example.petstoremobile.api.CategoryApi; -import com.example.petstoremobile.api.InventoryApi; -import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.InventoryViewModel; +import com.example.petstoremobile.utils.Resource; import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { @@ -55,8 +49,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private final List inventoryList = new ArrayList<>(); private final List categoryList = new ArrayList<>(); private InventoryAdapter adapter; - @Inject InventoryApi inventoryApi; - @Inject CategoryApi categoryApi; + private InventoryViewModel viewModel; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; @@ -82,7 +75,13 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private boolean spinnerReady = false; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory, container, false); @@ -117,21 +116,13 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn // Categories private void loadCategories() { - categoryApi.getAllCategories(0, 100).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, - Response> response) { - if (response.isSuccessful() && response.body() != null) { - categoryList.clear(); - categoryList.addAll(response.body().getContent()); - setupCategorySpinner(); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - Log.e(TAG, "Failed to load categories", t); - // Still setup spinner with just "All" + viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + categoryList.clear(); + categoryList.addAll(resource.data.getContent()); + setupCategorySpinner(); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + Log.e(TAG, "Failed to load categories: " + resource.message); setupCategorySpinner(); } }); @@ -145,33 +136,35 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn categoryNames.add(c.getCategoryName()); } - BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>( - requireContext(), - android.R.layout.simple_spinner_item, - categoryNames); - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerCategory.setAdapter(spinnerAdapter); + if (getContext() != null) { + BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>( + requireContext(), + android.R.layout.simple_spinner_item, + categoryNames); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerCategory.setAdapter(spinnerAdapter); - spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (!spinnerReady) { - // Skip the first automatic trigger on setup - spinnerReady = true; - return; + spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (!spinnerReady) { + // Skip the first automatic trigger on setup + spinnerReady = true; + return; + } + if (position == 0) { + selectedCategory = null; // "All Categories" + } else { + selectedCategory = categoryList.get(position - 1).getCategoryName(); + } + loadInventory(true); } - if (position == 0) { - selectedCategory = null; // "All Categories" - } else { - selectedCategory = categoryList.get(position - 1).getCategoryName(); - } - loadInventory(true); - } - @Override - public void onNothingSelected(AdapterView parent) { - } - }); + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } } // Search @@ -209,7 +202,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn rv.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy <= 0) return; LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); @@ -230,11 +223,10 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); } - // Load inventory + // Helper function to get a list of all inventory items from the backend private void loadInventory(boolean reset) { if (isLoading) return; - isLoading = true; if (reset) { currentPage = 0; @@ -244,39 +236,38 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn // Build query: combine search text + selected category String q = buildQuery(); - inventoryApi.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc") - .enqueue(new Callback>() { - @Override - public void onResponse(Call> call, - Response> response) { - isLoading = false; - if (swipeRefreshLayout != null) - swipeRefreshLayout.setRefreshing(false); + //Load all inventory items from the backend using viewModel + viewModel.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc").observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; - if (response.isSuccessful() && response.body() != null) { - PageResponse page = response.body(); - if (reset) - inventoryList.clear(); - inventoryList.addAll(page.getContent()); - adapter.notifyDataSetChanged(); - isLastPage = page.isLast(); - if (!isLastPage) - currentPage++; - } else { - Log.e(TAG, "Error " + response.code()); - Toast.makeText(getContext(), "Failed to load inventory", Toast.LENGTH_SHORT).show(); - } + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + isLoading = true; + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + isLoading = false; + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (resource.data != null) { + if (reset) inventoryList.clear(); + inventoryList.addAll(resource.data.getContent()); + adapter.notifyDataSetChanged(); + isLastPage = resource.data.isLast(); + if (!isLastPage) currentPage++; } - - @Override - public void onFailure(Call> call, Throwable t) { - isLoading = false; - if (swipeRefreshLayout != null) - swipeRefreshLayout.setRefreshing(false); - Log.e(TAG, "Network error", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); + break; + case ERROR: + // Hide loading indicator and toast error message + isLoading = false; + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + Log.e(TAG, "Error: " + resource.message); + Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); + break; + } + }); } // Combines search text and category into one query string for ?q= @@ -308,25 +299,18 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } private void bulkDelete(List ids) { - inventoryApi.bulkDeleteInventory(new BulkDeleteRequest(ids)) - .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - adapter.clearSelection(); - hideBulkDeleteBar(); - loadInventory(true); - Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + adapter.clearSelection(); + hideBulkDeleteBar(); + loadInventory(true); + Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + } + }); } private void hideBulkDeleteBar() { @@ -374,4 +358,4 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn hideBulkDeleteBar(); } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index ac456828..fdbc38f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -25,10 +26,10 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; -import com.example.petstoremobile.api.PetApi; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; @@ -38,9 +39,6 @@ import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { @@ -48,13 +46,19 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private List filteredList = new ArrayList<>(); private ImageButton hamburger; private PetAdapter adapter; - @Inject PetApi api; + private PetViewModel viewModel; + @Inject @Named("baseUrl") String baseUrl; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; private Spinner spinnerStatus; - //load pet view + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PetViewModel.class); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -67,12 +71,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupStatusFilter(view); setupSwipeRefresh(view); - - //Add button to opens the add dialog FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet); fabAddPet.setOnClickListener(v -> openPetDetails(-1)); - //Make the hamburger button open the drawer from listFragment hamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { @@ -103,7 +104,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } - //Setup the status filter spinner private void setupStatusFilter(View view) { spinnerStatus = view.findViewById(R.id.spinnerStatus); String[] statuses = {"All Statuses", "Available", "Adopted"}; @@ -122,7 +122,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } - // Helper function to filter pets based on search and status filter private void filterPets() { String query = etSearch.getText().toString().toLowerCase(); String selectedStatus = spinnerStatus.getSelectedItem().toString(); @@ -146,14 +145,10 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadPetData(); - }); + swipeRefreshLayout.setOnRefreshListener(this::loadPetData); } - //Open pet profile private void openPetProfile(int position) { - //Make a bundle to pass data to the profile fragment Bundle args = new Bundle(); PetDTO pet = filteredList.get(position); args.putInt("petId", pet.getPetId().intValue()); @@ -172,12 +167,10 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } - //Open the pet detail view for adding private void openPetDetails(int position) { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } - // Called by PetAdapter when a row is clicked to open the details view @Override public void onPetClick(int position) { openPetProfile(position); @@ -185,38 +178,35 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen // Helper function to get a list of all pets from the backend private void loadPetData() { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(true); - } - api.getAllPets(0, 100).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - if (response.isSuccessful() && response.body() != null) { - petList.clear(); - petList.addAll(response.body().getContent()); - filterPets(); - - } else { - Log.e("onResponse: ", response.message()); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - Toast.makeText(getContext(), - "Failed to load pets", Toast.LENGTH_SHORT).show(); - Log.e("onFailure: ", t.getMessage()); + //Load all pets from the backend using viewModel + viewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (resource.data != null) { + petList.clear(); + petList.addAll(resource.data.getContent()); + filterPets(); + } + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("PetFragment", "Error loading pets: " + resource.message); + break; } }); } - //set up the recyclerview and adapter private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); adapter = new PetAdapter(filteredList, this); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 16d6347f..d56da91a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -5,18 +5,21 @@ import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; -import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; @@ -24,7 +27,6 @@ import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; @AndroidEntryPoint public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { @@ -34,13 +36,19 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private ProductAdapter adapter; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + private ProductViewModel viewModel; - @Inject ProductApi api; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_product, container, false); @@ -109,27 +117,35 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc adapter.notifyDataSetChanged(); } + // Helper function to get a list of all products from the backend private void loadProducts() { - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - api.getAllProducts(null, 0, 100) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - if (r.isSuccessful() && r.body() != null) { - productList.clear(); - productList.addAll(r.body().getContent()); - filter(); - } else { - Toast.makeText(getContext(), "Failed to load products", - Toast.LENGTH_SHORT).show(); - } + //Load all products from the backend using viewModel + viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (resource.data != null) { + productList.clear(); + productList.addAll(resource.data.getContent()); + filter(); } - public void onFailure(Call> c, Throwable t) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - Log.e("ProductFragment", t.getMessage()); - } - }); + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("ProductFragment", "Error loading products: " + resource.message); + break; + } + }); } private void openDetail(int position) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 18d97c9d..a7b16298 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -5,25 +5,24 @@ import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; -import com.example.petstoremobile.api.ProductSupplierApi; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductSupplierDetailFragment; +import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; +import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; @AndroidEntryPoint public class ProductSupplierFragment extends Fragment @@ -34,11 +33,16 @@ public class ProductSupplierFragment extends Fragment private ProductSupplierAdapter adapter; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; - - @Inject ProductSupplierApi api; + private ProductSupplierViewModel viewModel; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_product_supplier, container, false); @@ -103,27 +107,35 @@ public class ProductSupplierFragment extends Fragment adapter.notifyDataSetChanged(); } + // Helper function to get a list of all product suppliers from the backend private void loadData() { - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); - api.getAllProductSuppliers(0, 100) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - if (r.isSuccessful() && r.body() != null) { - psList.clear(); - psList.addAll(r.body().getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); - } else { - Toast.makeText(getContext(), "Failed to load", - Toast.LENGTH_SHORT).show(); - } + //Load all product suppliers from the backend using viewModel + viewModel.getAllProductSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (resource.data != null) { + psList.clear(); + psList.addAll(resource.data.getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); } - public void onFailure(Call> c, Throwable t) { - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); - Log.e("PSFragment", t.getMessage()); - } - }); + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("PSFragment", "Error loading: " + resource.message); + break; + } + }); } private void openDetail(int position) { @@ -141,4 +153,4 @@ public class ProductSupplierFragment extends Fragment @Override public void onProductSupplierClick(int position) { openDetail(position); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 9a7226a8..35366e83 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -5,23 +5,23 @@ import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PurchaseOrderAdapter; -import com.example.petstoremobile.api.PurchaseOrderApi; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; +import com.example.petstoremobile.utils.Resource; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; @AndroidEntryPoint public class PurchaseOrderFragment extends Fragment @@ -32,8 +32,13 @@ public class PurchaseOrderFragment extends Fragment private PurchaseOrderAdapter adapter; private SwipeRefreshLayout swipeRefresh; private EditText etSearch; + private PurchaseOrderViewModel viewModel; - @Inject PurchaseOrderApi api; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -75,7 +80,7 @@ public class PurchaseOrderFragment extends Fragment public void afterTextChanged(Editable s) { } - public void onTextChanged(CharSequence s, int a, int b, int c) { + public void onTextChanged(CharSequence s, int start, int before, int count) { filter(s.toString()); } }); @@ -102,31 +107,35 @@ public class PurchaseOrderFragment extends Fragment adapter.notifyDataSetChanged(); } + // Helper function to get a list of all purchase orders from the backend private void loadData() { - if (swipeRefresh != null) - swipeRefresh.setRefreshing(true); - api.getAllPurchaseOrders(0, 100) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (swipeRefresh != null) - swipeRefresh.setRefreshing(false); - if (r.isSuccessful() && r.body() != null) { - poList.clear(); - poList.addAll(r.body().getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); - } else { - Toast.makeText(getContext(), "Failed to load purchase orders", - Toast.LENGTH_SHORT).show(); - } - } + //Load all purchase orders from the backend using viewModel + viewModel.getAllPurchaseOrders(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; - public void onFailure(Call> c, Throwable t) { - if (swipeRefresh != null) - swipeRefresh.setRefreshing(false); - Log.e("POFragment", t.getMessage()); + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (resource.data != null) { + poList.clear(); + poList.addAll(resource.data.getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); } - }); + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show(); + Log.e("POFragment", "Error loading purchase orders: " + resource.message); + break; + } + }); } private void openDetail(int position) { @@ -143,4 +152,4 @@ public class PurchaseOrderFragment extends Fragment public void onPurchaseOrderClick(int position) { openDetail(position); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 9ea74d19..859bacca 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -1,6 +1,8 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index dc9ef0fe..1c12cbf8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -22,21 +23,16 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ServiceAdapter; -import com.example.petstoremobile.api.ServiceApi; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { @@ -45,13 +41,19 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private List filteredList = new ArrayList<>(); private ServiceAdapter adapter; private ImageButton hamburger; - @Inject ServiceApi api; + private ServiceViewModel viewModel; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + } + //load service view @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_service, container, false); @@ -98,8 +100,8 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic } else { String lower = query.toLowerCase(); for (ServiceDTO s : serviceList) { - if (s.getServiceName().toLowerCase().contains(lower) - || s.getServiceDesc().toLowerCase().contains(lower)) { + if ((s.getServiceName() != null && s.getServiceName().toLowerCase().contains(lower)) + || (s.getServiceDesc() != null && s.getServiceDesc().toLowerCase().contains(lower))) { filteredList.add(s); } } @@ -109,9 +111,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadServiceData(); - }); + swipeRefreshLayout.setOnRefreshListener(this::loadServiceData); } //Open the service detail view depending on the mode @@ -141,35 +141,33 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic // Helper function to get a list of all services from the backend private void loadServiceData() { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(true); - } - api.getAllServices(0, 100).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - if (response.isSuccessful() && response.body() != null) { - serviceList.clear(); - serviceList.addAll(response.body().getContent()); - filterServices(etSearch.getText().toString()); + //Load all services from the backend using viewModel + viewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; - } else { - Log.e("onResponse: ", response.message()); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - if (getContext() != null) { - Toast.makeText(getContext(), - "Failed to load services", Toast.LENGTH_SHORT).show(); - } - Log.e("onFailure: ", t.getMessage()); + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (resource.data != null) { + serviceList.clear(); + serviceList.addAll(resource.data.getContent()); + filterServices(etSearch != null ? etSearch.getText().toString() : ""); + } + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); + } + Log.e("ServiceFragment", "Error loading services: " + resource.message); + break; } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index f9de9227..5e26a432 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -22,21 +23,16 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; -import com.example.petstoremobile.api.SupplierApi; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.viewmodels.SupplierViewModel; +import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { @@ -45,13 +41,19 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private List filteredList = new ArrayList<>(); private SupplierAdapter adapter; private ImageButton hamburger; - @Inject SupplierApi api; + private SupplierViewModel viewModel; private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + } + //load supplier view @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_supplier, container, false); @@ -98,9 +100,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } else { String lower = query.toLowerCase(); for (SupplierDTO s : supplierList) { - if (s.getSupCompany().toLowerCase().contains(lower) - || s.getSupContactFirstName().toLowerCase().contains(lower) - || s.getSupContactLastName().toLowerCase().contains(lower)) { + if ((s.getSupCompany() != null && s.getSupCompany().toLowerCase().contains(lower)) + || (s.getSupContactFirstName() != null && s.getSupContactFirstName().toLowerCase().contains(lower)) + || (s.getSupContactLastName() != null && s.getSupContactLastName().toLowerCase().contains(lower))) { filteredList.add(s); } } @@ -110,9 +112,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadSupplierData(); - }); + swipeRefreshLayout.setOnRefreshListener(this::loadSupplierData); } //Open the supplier detail view depending on the mode @@ -144,35 +144,33 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp // Helper function to get a list of all suppliers from the backend private void loadSupplierData() { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(true); - } - api.getAllSuppliers(0, 100).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - if (response.isSuccessful() && response.body() != null) { - supplierList.clear(); - supplierList.addAll(response.body().getContent()); - filterSuppliers(etSearch.getText().toString()); + //Load all suppliers from the backend using viewModel + viewModel.getAllSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; - } else { - Log.e("onResponse: ", response.message()); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - if (getContext() != null) { - Toast.makeText(getContext(), - "Failed to load suppliers", Toast.LENGTH_SHORT).show(); - } - Log.e("onFailure: ", t.getMessage()); + // Check the status to see if the resource is loaded and display the data + switch (resource.status) { + case LOADING: + // Show loading indicator + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + break; + case SUCCESS: + // Hide loading indicator and display data + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (resource.data != null) { + supplierList.clear(); + supplierList.addAll(resource.data.getContent()); + filterSuppliers(etSearch.getText().toString()); + } + break; + case ERROR: + // Hide loading indicator and toast error message + if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show(); + } + Log.e("SupplierFragment", "Error loading suppliers: " + resource.message); + break; } }); } @@ -184,4 +182,4 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(adapter); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index bb830f51..0eecece6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -13,10 +13,12 @@ import android.widget.*; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.bumptech.glide.Glide; @@ -28,9 +30,11 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.FileUtils; + import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; import java.math.BigDecimal; import java.util.*; @@ -41,7 +45,6 @@ import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; -import retrofit2.*; @AndroidEntryPoint public class ProductDetailFragment extends Fragment { @@ -61,9 +64,8 @@ public class ProductDetailFragment extends Fragment { private List categoryList = new ArrayList<>(); private Uri photoUri; + private ProductViewModel viewModel; - @Inject ProductApi productApi; - @Inject CategoryApi categoryApi; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; @@ -74,12 +76,13 @@ public class ProductDetailFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ProductViewModel.class); + galleryLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri selectedImage = result.getData().getData(); - // Update image view locally Glide.with(this).load(selectedImage).into(ivProductImage); photoUri = selectedImage; hasImage = true; @@ -92,7 +95,6 @@ public class ProductDetailFragment extends Fragment { new ActivityResultContracts.TakePicture(), success -> { if (success) { - // Update image view locally Glide.with(this).load(photoUri).into(ivProductImage); hasImage = true; isImageChanged = true; @@ -185,19 +187,12 @@ public class ProductDetailFragment extends Fragment { // Helper function to load categories from the backend for the spinner private void loadCategories() { - categoryApi.getAllCategories(0, 100) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - categoryList = r.body().getContent(); - populateCategorySpinner(); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("ProductDetail", "Category load failed: " + t.getMessage()); - } - }); + viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + categoryList = resource.data.getContent(); + populateCategorySpinner(); + } + }); } // Helper function to populate the category spinner @@ -265,24 +260,19 @@ public class ProductDetailFragment extends Fragment { // updating/adding photo, removing photo or no change private void performPendingImageActions(String successMsg) { if (isImageRemoved) { - //if the image is removed then delete the image - productApi.deleteProductImage(prodId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); - navigateBack(); - } - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); + viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); + } navigateBack(); } }); } else if (isImageChanged && photoUri != null) { - //if the image is changed then upload it uploadProductImageAndNavigate(photoUri, successMsg); } else { - //if no changes then navigate back Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); navigateBack(); } @@ -291,61 +281,28 @@ public class ProductDetailFragment extends Fragment { // Helper function to upload the product image by calling the backend // and then navigate back to the previous screen private void uploadProductImageAndNavigate(Uri uri, String successMsg) { - try { - File file = getFileFromUri(uri); - if (file == null) { - Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); - navigateBack(); - return; - } - - RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); - MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - - productApi.uploadProductImage(prodId, body) - .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); - } - navigateBack(); - } - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), successMsg + " (network error during upload)", Toast.LENGTH_SHORT).show(); - navigateBack(); - } - }); - } catch (Exception e) { - Log.e("ProductDetail", "Error uploading image", e); + File file = FileUtils.getFileFromUri(requireContext(), uri); + if (file == null) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); navigateBack(); + return; } - } - // Helper function to get the File from the Uri - private File getFileFromUri(Uri uri) { - try { - InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); - File tempFile = new File(requireContext().getCacheDir(), "upload_product_image.jpg"); - FileOutputStream outputStream = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, length); + RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); + MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); + + viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); + } + navigateBack(); } - outputStream.close(); - inputStream.close(); - return tempFile; - } catch (Exception e) { - return null; - } + }); } - // Function to save the product to the server private void saveProduct() { String name = etProductName.getText().toString().trim(); String desc = etProductDesc.getText().toString().trim(); @@ -372,35 +329,25 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); if (isEditing) { - productApi.updateProduct(prodId, dto).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { + viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { performPendingImageActions("Updated"); } else { - Toast.makeText(getContext(), "Error " + response.code(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } } - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } }); } else { - productApi.createProduct(dto).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - prodId = response.body().getProdId(); + viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + prodId = resource.data.getProdId(); performPendingImageActions("Saved"); } else { - Toast.makeText(getContext(), "Error saving", Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Error saving: " + resource.message, Toast.LENGTH_SHORT).show(); } } - @Override - public void onFailure(Call call, Throwable t) { - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } }); } } @@ -410,20 +357,17 @@ public class ProductDetailFragment extends Fragment { new AlertDialog.Builder(requireContext()) .setTitle("Delete Product?") .setPositiveButton("Yes", (d, w) -> - productApi.deleteProduct(prodId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - navigateBack(); - } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", - Toast.LENGTH_SHORT).show(); - } - })) + viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource != null && resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + })) .setNegativeButton("No", null).show(); } private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java b/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java deleted file mode 100644 index e227bc9b..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.example.petstoremobile.models; - -public class Adoption { - private int adoptionId; - private String adopterName; - private String adopterEmail; - private String adopterPhone; - private String petName; - private String adoptionDate; - private String status; - - // Constructor - public Adoption(int adoptionId, String adopterName, String adopterEmail, String adopterPhone, String petName, String adoptionDate, String status) { - this.adoptionId = adoptionId; - this.adopterName = adopterName; - this.adopterEmail = adopterEmail; - this.adopterPhone = adopterPhone; - this.petName = petName; - this.adoptionDate = adoptionDate; - this.status = status; - } - - // Getters and setters - public int getAdoptionId() { - return adoptionId; - } - - public void setAdoptionId(int adoptionId) { - this.adoptionId = adoptionId; - } - - public String getAdopterName() { - return adopterName; - } - - public void setAdopterName(String adopterName) { - this.adopterName = adopterName; - } - - public String getAdopterEmail() { - return adopterEmail; - } - - public void setAdopterEmail(String adopterEmail) { - this.adopterEmail = adopterEmail; - } - - public String getAdopterPhone() { - return adopterPhone; - } - - public void setAdopterPhone(String adopterPhone) { - this.adopterPhone = adopterPhone; - } - - public String getPetName() { - return petName; - } - - public void setPetName(String petName) { - this.petName = petName; - } - - public String getAdoptionDate() { - return adoptionDate; - } - - public void setAdoptionDate(String adoptionDate) { - this.adoptionDate = adoptionDate; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java b/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java deleted file mode 100644 index 38e8da10..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.example.petstoremobile.models; - - -public class Appointment { - private int appointmentId; - private String customerName; - private String petName; - private String serviceType; - private String appointmentDate; - private String appointmentTime; - private String status; - - // Constructor - public Appointment(int appointmentId, String customerName, String petName, String serviceType, String appointmentDate, String appointmentTime, String status) { - this.appointmentId = appointmentId; - this.customerName = customerName; - this.petName = petName; - this.serviceType = serviceType; - this.appointmentDate = appointmentDate; - this.appointmentTime = appointmentTime; - this.status = status; - } - - // Getters and setters - public int getAppointmentId() { - return appointmentId; - } - - public String getCustomerName() { - return customerName; - } - - public void setCustomerName(String customerName) { - this.customerName = customerName; - } - - public String getPetName() { - return petName; - } - - public void setPetName(String petName) { - this.petName = petName; - } - - public String getServiceType() { - return serviceType; - } - - public void setServiceType(String serviceType) { - this.serviceType = serviceType; - } - - public String getAppointmentDate() { - return appointmentDate; - } - - public void setAppointmentDate(String appointmentDate) { - this.appointmentDate = appointmentDate; - } - - public String getAppointmentTime() { - return appointmentTime; - } - - public void setAppointmentTime(String appointmentTime) { - this.appointmentTime = appointmentTime; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java b/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java deleted file mode 100644 index 7aacd2df..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.example.petstoremobile.models; - - -public class Inventory { - private int inventoryId; - private String itemName; - private String category; - private int quantity; - private double unitPrice; - private String supplier; - - // Constructor - public Inventory(int inventoryId, String itemName, String category, int quantity, double unitPrice, String supplier) { - this.inventoryId = inventoryId; - this.itemName = itemName; - this.category = category; - this.quantity = quantity; - this.unitPrice = unitPrice; - this.supplier = supplier; - } - - // Getters and setters - public int getInventoryId() { - return inventoryId; - } - - public String getItemName() { - return itemName; - } - - public void setItemName(String itemName) { - this.itemName = itemName; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public int getQuantity() { - return quantity; - } - - public void setQuantity(int quantity) { - this.quantity = quantity; - } - - public double getUnitPrice() { - return unitPrice; - } - - public void setUnitPrice(double unitPrice) { - this.unitPrice = unitPrice; - } - - public String getSupplier() { - return supplier; - } - - public void setSupplier(String supplier) { - this.supplier = supplier; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Product.java b/android/app/src/main/java/com/example/petstoremobile/models/Product.java deleted file mode 100644 index 90a56eab..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/Product.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.example.petstoremobile.models; - - -public class Product { - private int productId; - private String productName; - private String productDesc; - private String category; - private double productPrice; - private int stockQuantity; - - // Constructor - public Product(int productId, String productName, String productDesc, String category, double productPrice, int stockQuantity) { - this.productId = productId; - this.productName = productName; - this.productDesc = productDesc; - this.category = category; - this.productPrice = productPrice; - this.stockQuantity = stockQuantity; - } - - // Getters and setters - public int getProductId() { - return productId; - } - - public String getProductName() { - return productName; - } - - public void setProductName(String productName) { - this.productName = productName; - } - - public String getProductDesc() { - return productDesc; - } - - public void setProductDesc(String productDesc) { - this.productDesc = productDesc; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public double getProductPrice() { - return productPrice; - } - - public void setProductPrice(double productPrice) { - this.productPrice = productPrice; - } - - public int getStockQuantity() { - return stockQuantity; - } - - public void setStockQuantity(int stockQuantity) { - this.stockQuantity = stockQuantity; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java b/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java deleted file mode 100644 index b624b5e4..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/ProductSupplier.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.petstoremobile.models; - -public class ProductSupplier { - private int supId; - private int prodId; - private String supCompany; - private String prodName; - private double cost; - - public ProductSupplier(int supId, int prodId, String supCompany, String prodName, double cost) { - this.supId = supId; - this.prodId = prodId; - this.supCompany = supCompany; - this.prodName = prodName; - this.cost = cost; - } - - public int getSupId() { - return supId; - } - - public int getProdId() { - return prodId; - } - - public String getSupCompany() { - return supCompany; - } - - public String getProdName() { - return prodName; - } - - public double getCost() { - return cost; - } - - public void setSupCompany(String supCompany) { - this.supCompany = supCompany; - } - - public void setProdName(String prodName) { - this.prodName = prodName; - } - - public void setCost(double cost) { - this.cost = cost; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/PurchaseOrder.java b/android/app/src/main/java/com/example/petstoremobile/models/PurchaseOrder.java deleted file mode 100644 index 971ff400..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/models/PurchaseOrder.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.petstoremobile.models; - -public class PurchaseOrder { - private int purchaseOrderId; - private String supplierName; - private String orderDate; - private String status; - - public PurchaseOrder(int purchaseOrderId, String supplierName, String orderDate, String status) { - this.purchaseOrderId = purchaseOrderId; - this.supplierName = supplierName; - this.orderDate = orderDate; - this.status = status; - } - - public int getPurchaseOrderId() { - return purchaseOrderId; - } - - public String getSupplierName() { - return supplierName; - } - - public String getOrderDate() { - return orderDate; - } - - public String getStatus() { - return status; - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java new file mode 100644 index 00000000..8164d34b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -0,0 +1,156 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.AdoptionApi; +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class AdoptionRepository { + private final AdoptionApi adoptionApi; + + @Inject + public AdoptionRepository(AdoptionApi adoptionApi) { + this.adoptionApi = adoptionApi; + } + + /** + * Retrieves a paginated list of all adoptions from the API. + */ + public LiveData>> getAllAdoptions(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + adoptionApi.getAllAdoptions(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific adoption record by its ID from the API. + */ + public LiveData> getAdoptionById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + adoptionApi.getAdoptionById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new adoption record. + */ + public LiveData> createAdoption(AdoptionDTO adoption) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + adoptionApi.createAdoption(adoption).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing adoption record by ID. + */ + public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + adoptionApi.updateAdoption(id, adoption).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific adoption record. + */ + public LiveData> deleteAdoption(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + adoptionApi.deleteAdoption(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java new file mode 100644 index 00000000..cea28d0a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -0,0 +1,156 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.AppointmentApi; +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class AppointmentRepository { + private final AppointmentApi appointmentApi; + + @Inject + public AppointmentRepository(AppointmentApi appointmentApi) { + this.appointmentApi = appointmentApi; + } + + /** + * Retrieves a paginated list of all appointments from the API. + */ + public LiveData>> getAllAppointments(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + appointmentApi.getAllAppointments(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific appointment by its ID from the API. + */ + public LiveData> getAppointmentById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + appointmentApi.getAppointmentById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new appointment record. + */ + public LiveData> createAppointment(AppointmentDTO appointment) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + appointmentApi.createAppointment(appointment).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing appointment record by ID. + */ + public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + appointmentApi.updateAppointment(id, appointment).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific appointment record. + */ + public LiveData> deleteAppointment(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + appointmentApi.deleteAppointment(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java new file mode 100644 index 00000000..c89de328 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -0,0 +1,174 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.auth.AuthApi; +import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.dtos.AuthDTO; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.utils.Resource; + +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class AuthRepository { + private final AuthApi authApi; + private final TokenManager tokenManager; + + @Inject + public AuthRepository(AuthApi authApi, TokenManager tokenManager) { + this.authApi = authApi; + this.tokenManager = tokenManager; + } + + /** + * Authenticates the user and saves login data (token, username, role) upon success. + */ + public LiveData> login(AuthDTO.LoginRequest loginRequest) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + authApi.login(loginRequest).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + tokenManager.saveLoginData( + response.body().getToken(), + response.body().getUsername(), + response.body().getRole() + ); + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Login failed: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves the current user's profile information from the API. + */ + public LiveData> getMe() { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + authApi.getMe().enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Updates the current user's profile details. + */ + public LiveData> updateMe(Map updates) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + authApi.updateMe(updates).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Uploads a multipart image to be used as the current user's avatar. + */ + public LiveData> uploadAvatar(MultipartBody.Part avatar) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + authApi.uploadAvatar(avatar).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to remove the current user's avatar. + */ + public LiveData> deleteAvatar() { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + authApi.deleteAvatar().enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Clears all authentication and login data from storage. + */ + public void logout() { + tokenManager.clearLoginData(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java new file mode 100644 index 00000000..7837f582 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -0,0 +1,52 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.CategoryApi; +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class CategoryRepository { + private final CategoryApi categoryApi; + + @Inject + public CategoryRepository(CategoryApi categoryApi) { + this.categoryApi = categoryApi; + } + + /** + * Retrieves a paginated list of all product categories from the API. + */ + public LiveData>> getAllCategories(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + categoryApi.getAllCategories(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java new file mode 100644 index 00000000..ee2503cd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -0,0 +1,184 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.InventoryApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.InventoryRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class InventoryRepository { + private final InventoryApi inventoryApi; + + @Inject + public InventoryRepository(InventoryApi inventoryApi) { + this.inventoryApi = inventoryApi; + } + + /** + * Retrieves a paginated list of inventory items from the API with optional search and sort. + */ + public LiveData>> getAllInventory(String query, int page, int size, String sort) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.getAllInventory(query, page, size, sort).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific inventory item by its ID from the API. + */ + public LiveData> getInventoryById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.getInventoryById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new inventory record. + */ + public LiveData> createInventory(InventoryRequest request) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.createInventory(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing inventory record. + */ + public LiveData> updateInventory(Long id, InventoryRequest request) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.updateInventory(id, request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific inventory record. + */ + public LiveData> deleteInventory(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.deleteInventory(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete multiple inventory records at once. + */ + public LiveData> bulkDeleteInventory(BulkDeleteRequest request) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + inventoryApi.bulkDeleteInventory(request).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java new file mode 100644 index 00000000..0737e569 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -0,0 +1,208 @@ +package com.example.petstoremobile.repositories; + +import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +@Singleton +public class PetRepository { + private final PetApi petApi; + + @Inject + public PetRepository(PetApi petApi) { + this.petApi = petApi; + } + + /** + * Retrieves a paginated list of all pets from the API. + */ + public LiveData>> getAllPets(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.getAllPets(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific pet by its ID from the API. + */ + public LiveData> getPetById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.getPetById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new pet record. + */ + public LiveData> createPet(PetDTO pet) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.createPet(pet).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing pet record. + */ + public LiveData> updatePet(Long id, PetDTO pet) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.updatePet(id, pet).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific pet record. + */ + public LiveData> deletePet(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.deletePet(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Uploads an image file for a specific pet via the API. + */ + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.uploadPetImage(id, image).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete the image of a specific pet. + */ + public LiveData> deletePetImage(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + petApi.deletePetImage(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java new file mode 100644 index 00000000..df79335f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -0,0 +1,209 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class ProductRepository { + private final ProductApi productApi; + + @Inject + public ProductRepository(ProductApi productApi) { + this.productApi = productApi; + } + + /** + * Retrieves a paginated list of products from the API, filtered by an optional query. + */ + public LiveData>> getAllProducts(String query, int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.getAllProducts(query, page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific product by its ID from the API. + */ + public LiveData> getProductById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.getProductById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new product. + */ + public LiveData> createProduct(ProductDTO product) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.createProduct(product).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing product by ID. + */ + public LiveData> updateProduct(Long id, ProductDTO product) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.updateProduct(id, product).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific product. + */ + public LiveData> deleteProduct(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.deleteProduct(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Uploads an image file for a specific product via the API. + */ + public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.uploadProductImage(id, image).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete the image of a specific product. + */ + public LiveData> deleteProductImage(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + productApi.deleteProductImage(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java new file mode 100644 index 00000000..2e961f6c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -0,0 +1,130 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.ProductSupplierApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class ProductSupplierRepository { + private final ProductSupplierApi api; + + @Inject + public ProductSupplierRepository(ProductSupplierApi api) { + this.api = api; + } + + /** + * Retrieves a paginated list of all product-supplier relationships from the API. + */ + public LiveData>> getAllProductSuppliers(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.getAllProductSuppliers(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new product-supplier relationship. + */ + public LiveData> createProductSupplier(ProductSupplierDTO dto) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.createProductSupplier(dto).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing product-supplier relationship. + */ + public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.updateProductSupplier(productId, supplierId, dto).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific product-supplier relationship. + */ + public LiveData> deleteProductSupplier(Long productId, Long supplierId) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.deleteProductSupplier(productId, supplierId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java new file mode 100644 index 00000000..f804c2e3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.PurchaseOrderApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class PurchaseOrderRepository { + private final PurchaseOrderApi api; + + @Inject + public PurchaseOrderRepository(PurchaseOrderApi api) { + this.api = api; + } + + /** + * Retrieves a paginated list of all purchase orders from the API. + */ + public LiveData>> getAllPurchaseOrders(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.getAllPurchaseOrders(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific purchase order by its ID from the API. + */ + public LiveData> getPurchaseOrderById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + api.getPurchaseOrderById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java new file mode 100644 index 00000000..8d81ccfb --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -0,0 +1,156 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.ServiceApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class ServiceRepository { + private final ServiceApi serviceApi; + + @Inject + public ServiceRepository(ServiceApi serviceApi) { + this.serviceApi = serviceApi; + } + + /** + * Retrieves a paginated list of all services from the API. + */ + public LiveData>> getAllServices(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + serviceApi.getAllServices(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific service by its ID from the API. + */ + public LiveData> getServiceById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + serviceApi.getServiceById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new service. + */ + public LiveData> createService(ServiceDTO service) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + serviceApi.createService(service).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing service by ID. + */ + public LiveData> updateService(Long id, ServiceDTO service) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + serviceApi.updateService(id, service).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific service. + */ + public LiveData> deleteService(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + serviceApi.deleteService(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java new file mode 100644 index 00000000..eb7b8b61 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -0,0 +1,156 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.SupplierApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +@Singleton +public class SupplierRepository { + private final SupplierApi supplierApi; + + @Inject + public SupplierRepository(SupplierApi supplierApi) { + this.supplierApi = supplierApi; + } + + /** + * Retrieves a paginated list of all suppliers from the API. + */ + public LiveData>> getAllSuppliers(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + supplierApi.getAllSuppliers(page, size).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Retrieves a specific supplier by its ID from the API. + */ + public LiveData> getSupplierById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + supplierApi.getSupplierById(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to create a new supplier record. + */ + public LiveData> createSupplier(SupplierDTO supplier) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + supplierApi.createSupplier(supplier).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to update an existing supplier record by ID. + */ + public LiveData> updateSupplier(Long id, SupplierDTO supplier) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + supplierApi.updateSupplier(id, supplier).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + data.setValue(Resource.success(response.body())); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } + + /** + * Sends a request to the API to delete a specific supplier record. + */ + public LiveData> deleteSupplier(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + supplierApi.deleteSupplier(id).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(null)); + } else { + data.setValue(Resource.error("Error: " + response.message(), null)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + data.setValue(Resource.error(t.getMessage(), null)); + } + }); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java new file mode 100644 index 00000000..dcdc1bd7 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/FileUtils.java @@ -0,0 +1,27 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import android.net.Uri; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +public class FileUtils { + public static File getFileFromUri(Context context, Uri uri) { + try { + InputStream inputStream = context.getContentResolver().openInputStream(uri); + File tempFile = new File(context.getCacheDir(), "upload_image_" + System.currentTimeMillis() + ".jpg"); + FileOutputStream outputStream = new FileOutputStream(tempFile); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + outputStream.close(); + inputStream.close(); + return tempFile; + } catch (Exception e) { + return null; + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/Resource.java b/android/app/src/main/java/com/example/petstoremobile/utils/Resource.java new file mode 100644 index 00000000..4c766cc2 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/Resource.java @@ -0,0 +1,30 @@ +package com.example.petstoremobile.utils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class Resource { + public enum Status { SUCCESS, ERROR, LOADING } + + public final Status status; + public final T data; + public final String message; + + private Resource(Status status, @Nullable T data, @Nullable String message) { + this.status = status; + this.data = data; + this.message = message; + } + + public static Resource success(@Nullable T data) { + return new Resource<>(Status.SUCCESS, data, null); + } + + public static Resource error(String msg, @Nullable T data) { + return new Resource<>(Status.ERROR, data, msg); + } + + public static Resource loading(@Nullable T data) { + return new Resource<>(Status.LOADING, data, null); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java new file mode 100644 index 00000000..039cef30 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AdoptionViewModel.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AdoptionDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.AdoptionRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AdoptionViewModel extends ViewModel { + private final AdoptionRepository repository; + + @Inject + public AdoptionViewModel(AdoptionRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all adoptions. + */ + public LiveData>> getAllAdoptions(int page, int size) { + return repository.getAllAdoptions(page, size); + } + + /** + * Retrieves a single adoption by its ID. + */ + public LiveData> getAdoptionById(Long id) { + return repository.getAdoptionById(id); + } + + /** + * Creates a new adoption record. + */ + public LiveData> createAdoption(AdoptionDTO adoption) { + return repository.createAdoption(adoption); + } + + /** + * Updates an existing adoption record by ID. + */ + public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { + return repository.updateAdoption(id, adoption); + } + + /** + * Deletes an adoption record by ID. + */ + public LiveData> deleteAdoption(Long id) { + return repository.deleteAdoption(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java new file mode 100644 index 00000000..23db67d0 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.AppointmentRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class AppointmentViewModel extends ViewModel { + private final AppointmentRepository repository; + + @Inject + public AppointmentViewModel(AppointmentRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all appointments. + */ + public LiveData>> getAllAppointments(int page, int size) { + return repository.getAllAppointments(page, size); + } + + /** + * Retrieves a single appointment by its ID. + */ + public LiveData> getAppointmentById(Long id) { + return repository.getAppointmentById(id); + } + + /** + * Creates a new appointment. + */ + public LiveData> createAppointment(AppointmentDTO appointment) { + return repository.createAppointment(appointment); + } + + /** + * Updates an existing appointment record by ID. + */ + public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { + return repository.updateAppointment(id, appointment); + } + + /** + * Deletes an appointment record by ID. + */ + public LiveData> deleteAppointment(Long id) { + return repository.deleteAppointment(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java new file mode 100644 index 00000000..061ee687 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AuthViewModel.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.AuthDTO; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.repositories.AuthRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.Map; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class AuthViewModel extends ViewModel { + private final AuthRepository repository; + + @Inject + public AuthViewModel(AuthRepository repository) { + this.repository = repository; + } + + /** + * Authenticates a user with username and password. + */ + public LiveData> login(String username, String password) { + return repository.login(new AuthDTO.LoginRequest(username, password)); + } + + /** + * Retrieves the profile information of the currently authenticated user. + */ + public LiveData> getMe() { + return repository.getMe(); + } + + /** + * Updates the profile information of the current user. + */ + public LiveData> updateMe(Map updates) { + return repository.updateMe(updates); + } + + /** + * Uploads a new avatar image for the current user. + */ + public LiveData> uploadAvatar(MultipartBody.Part avatar) { + return repository.uploadAvatar(avatar); + } + + /** + * Deletes the avatar image of the current user. + */ + public LiveData> deleteAvatar() { + return repository.deleteAvatar(); + } + + /** + * Logs out the current user by clearing stored credentials. + */ + public void logout() { + repository.logout(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java new file mode 100644 index 00000000..3af31b5c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -0,0 +1,80 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.BulkDeleteRequest; +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.InventoryRequest; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class InventoryViewModel extends ViewModel { + private final InventoryRepository inventoryRepository; + private final CategoryRepository categoryRepository; + + @Inject + public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository) { + this.inventoryRepository = inventoryRepository; + this.categoryRepository = categoryRepository; + } + + /** + * Retrieves a paginated list of inventory items, with optional filtering and sorting. + */ + public LiveData>> getAllInventory(String query, int page, int size, String sort) { + return inventoryRepository.getAllInventory(query, page, size, sort); + } + + /** + * Retrieves a single inventory item by its ID. + */ + public LiveData> getInventoryById(Long id) { + return inventoryRepository.getInventoryById(id); + } + + /** + * Creates a new inventory record. + */ + public LiveData> createInventory(InventoryRequest request) { + return inventoryRepository.createInventory(request); + } + + /** + * Updates an existing inventory record by ID. + */ + public LiveData> updateInventory(Long id, InventoryRequest request) { + return inventoryRepository.updateInventory(id, request); + } + + /** + * Deletes an inventory record by ID. + */ + public LiveData> deleteInventory(Long id) { + return inventoryRepository.deleteInventory(id); + } + + /** + * Deletes multiple inventory records in a single request. + */ + public LiveData> bulkDeleteInventory(List ids) { + return inventoryRepository.bulkDeleteInventory(new BulkDeleteRequest(ids)); + } + + /** + * Retrieves a paginated list of categories. + */ + public LiveData>> getAllCategories(int page, int size) { + return categoryRepository.getAllCategories(page, size); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java new file mode 100644 index 00000000..8982dd51 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -0,0 +1,75 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.repositories.PetRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class PetViewModel extends ViewModel { + private final PetRepository repository; + private final MutableLiveData _petId = new MutableLiveData<>(); + + @Inject + public PetViewModel(PetRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all pets. + */ + public LiveData>> getAllPets(int page, int size) { + return repository.getAllPets(page, size); + } + + /** + * Retrieves a single pet by its ID. + */ + public LiveData> getPetById(Long id) { + return repository.getPetById(id); + } + + /** + * Creates a new pet record. + */ + public LiveData> createPet(PetDTO pet) { + return repository.createPet(pet); + } + + /** + * Updates an existing pet record by ID. + */ + public LiveData> updatePet(Long id, PetDTO pet) { + return repository.updatePet(id, pet); + } + + /** + * Deletes a pet record by ID. + */ + public LiveData> deletePet(Long id) { + return repository.deletePet(id); + } + + /** + * Uploads an image for a specific pet. + */ + public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { + return repository.uploadPetImage(id, image); + } + + /** + * Deletes the image associated with a specific pet. + */ + public LiveData> deletePetImage(Long id) { + return repository.deletePetImage(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java new file mode 100644 index 00000000..dbc55534 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java @@ -0,0 +1,51 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.repositories.ProductSupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ProductSupplierViewModel extends ViewModel { + private final ProductSupplierRepository repository; + + @Inject + public ProductSupplierViewModel(ProductSupplierRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all product-supplier relationships. + */ + public LiveData>> getAllProductSuppliers(int page, int size) { + return repository.getAllProductSuppliers(page, size); + } + + /** + * Creates a new product-supplier relationship. + */ + public LiveData> createProductSupplier(ProductSupplierDTO dto) { + return repository.createProductSupplier(dto); + } + + /** + * Updates an existing product-supplier relationship. + */ + public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { + return repository.updateProductSupplier(productId, supplierId, dto); + } + + /** + * Deletes a product-supplier relationship by product and supplier IDs. + */ + public LiveData> deleteProductSupplier(Long productId, Long supplierId) { + return repository.deleteProductSupplier(productId, supplierId); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java new file mode 100644 index 00000000..6edcdd4b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java @@ -0,0 +1,84 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CategoryDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.repositories.CategoryRepository; +import com.example.petstoremobile.repositories.ProductRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; + +@HiltViewModel +public class ProductViewModel extends ViewModel { + private final ProductRepository productRepository; + private final CategoryRepository categoryRepository; + + @Inject + public ProductViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) { + this.productRepository = productRepository; + this.categoryRepository = categoryRepository; + } + + /** + * Retrieves a paginated list of products, optionally filtered by a query string. + */ + public LiveData>> getAllProducts(String query, int page, int size) { + return productRepository.getAllProducts(query, page, size); + } + + /** + * Retrieves a single product by its ID. + */ + public LiveData> getProductById(Long id) { + return productRepository.getProductById(id); + } + + /** + * Creates a new product. + */ + public LiveData> createProduct(ProductDTO product) { + return productRepository.createProduct(product); + } + + /** + * Updates an existing product by ID. + */ + public LiveData> updateProduct(Long id, ProductDTO product) { + return productRepository.updateProduct(id, product); + } + + /** + * Deletes a product by its ID. + */ + public LiveData> deleteProduct(Long id) { + return productRepository.deleteProduct(id); + } + + /** + * Uploads an image for a specific product. + */ + public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { + return productRepository.uploadProductImage(id, image); + } + + /** + * Deletes the image associated with a specific product. + */ + public LiveData> deleteProductImage(Long id) { + return productRepository.deleteProductImage(id); + } + + /** + * Retrieves a paginated list of all product categories. + */ + public LiveData>> getAllCategories(int page, int size) { + return categoryRepository.getAllCategories(page, size); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java new file mode 100644 index 00000000..0b5d7ea1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java @@ -0,0 +1,37 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.repositories.PurchaseOrderRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class PurchaseOrderViewModel extends ViewModel { + private final PurchaseOrderRepository repository; + + @Inject + public PurchaseOrderViewModel(PurchaseOrderRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all purchase orders. + */ + public LiveData>> getAllPurchaseOrders(int page, int size) { + return repository.getAllPurchaseOrders(page, size); + } + + /** + * Retrieves a single purchase order by its ID. + */ + public LiveData> getPurchaseOrderById(Long id) { + return repository.getPurchaseOrderById(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java new file mode 100644 index 00000000..be4cee20 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.repositories.ServiceRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class ServiceViewModel extends ViewModel { + private final ServiceRepository repository; + + @Inject + public ServiceViewModel(ServiceRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all services. + */ + public LiveData>> getAllServices(int page, int size) { + return repository.getAllServices(page, size); + } + + /** + * Retrieves a single service by its ID. + */ + public LiveData> getServiceById(Long id) { + return repository.getServiceById(id); + } + + /** + * Creates a new service. + */ + public LiveData> createService(ServiceDTO service) { + return repository.createService(service); + } + + /** + * Updates an existing service by ID. + */ + public LiveData> updateService(Long id, ServiceDTO service) { + return repository.updateService(id, service); + } + + /** + * Deletes a service by ID. + */ + public LiveData> deleteService(Long id) { + return repository.deleteService(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java new file mode 100644 index 00000000..7885c898 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.repositories.SupplierRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class SupplierViewModel extends ViewModel { + private final SupplierRepository repository; + + @Inject + public SupplierViewModel(SupplierRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all suppliers. + */ + public LiveData>> getAllSuppliers(int page, int size) { + return repository.getAllSuppliers(page, size); + } + + /** + * Retrieves a single supplier by its ID. + */ + public LiveData> getSupplierById(Long id) { + return repository.getSupplierById(id); + } + + /** + * Creates a new supplier record. + */ + public LiveData> createSupplier(SupplierDTO supplier) { + return repository.createSupplier(supplier); + } + + /** + * Updates an existing supplier record by ID. + */ + public LiveData> updateSupplier(Long id, SupplierDTO supplier) { + return repository.updateSupplier(id, supplier); + } + + /** + * Deletes a supplier record by ID. + */ + public LiveData> deleteSupplier(Long id) { + return repository.deleteSupplier(id); + } +} From 5d956137860201c94c2a3cef21ca102b5c90e9f7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 12:17:37 -0600 Subject: [PATCH 054/137] Harden staff assignment --- .../petstoremobile/dtos/AdoptionDTO.java | 17 ++- .../petstoremobile/dtos/AppointmentDTO.java | 17 +++ .../controller/DropdownController.java | 36 ++++- .../backend/dto/adoption/AdoptionRequest.java | 16 ++- .../dto/adoption/AdoptionResponse.java | 22 +++- .../dto/appointment/AppointmentRequest.java | 16 ++- .../dto/appointment/AppointmentResponse.java | 18 +++ .../com/petshop/backend/entity/Adoption.java | 16 ++- .../petshop/backend/entity/Appointment.java | 16 ++- .../repository/EmployeeRepository.java | 2 + .../repository/EmployeeStoreRepository.java | 6 + .../backend/service/AdoptionService.java | 43 +++++- .../backend/service/AppointmentService.java | 39 ++++++ ...appointment_adoption_employee_required.sql | 61 +++++++++ .../controller/DropdownControllerTest.java | 90 +++++++++++++ .../backend/service/AdoptionServiceTest.java | 124 ++++++++++++++++++ .../service/AppointmentServiceTest.java | 70 ++++++++++ .../petshopdesktop/DTOs/AppointmentDTO.java | 10 +- .../api/dto/adoption/AdoptionRequest.java | 9 ++ .../api/dto/adoption/AdoptionResponse.java | 18 +++ .../dto/appointment/AppointmentRequest.java | 9 ++ .../dto/appointment/AppointmentResponse.java | 18 +++ .../api/endpoints/DropdownApi.java | 8 ++ .../controllers/AdoptionController.java | 6 + .../controllers/AppointmentController.java | 4 + .../AdoptionDialogController.java | 51 +++++++ .../AppointmentDialogController.java | 46 ++++++- .../petshopdesktop/models/Adoption.java | 18 ++- .../dialogviews/adoption-dialog-view.fxml | 15 +++ .../dialogviews/appointment-dialog-view.fxml | 27 +++- .../modelviews/adoption-view.fxml | 11 +- .../modelviews/appointment-view.fxml | 13 +- web/app/appointments/page.js | 45 +++++++ 33 files changed, 887 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql create mode 100644 backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java create mode 100644 backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index 03758473..6866f6b0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -8,6 +8,8 @@ public class AdoptionDTO { private String petName; private Long customerId; private String customerName; + private Long employeeId; + private String employeeName; private String adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; @@ -16,8 +18,13 @@ public class AdoptionDTO { // Constructor for create/update requests public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { + this(petId, customerId, null, adoptionDate, adoptionStatus); + } + + public AdoptionDTO(Long petId, Long customerId, Long employeeId, String adoptionDate, String adoptionStatus) { this.petId = petId; this.customerId = customerId; + this.employeeId = employeeId; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; } @@ -42,6 +49,14 @@ public class AdoptionDTO { return customerName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAdoptionDate() { return adoptionDate; } @@ -65,4 +80,4 @@ public class AdoptionDTO { public String getUpdatedAt() { return updatedAt; } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 4c7a91b7..05f9ea21 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -12,6 +12,8 @@ public class AppointmentDTO { private String storeName; private Long serviceId; private String serviceName; + private Long employeeId; + private String employeeName; private String appointmentDate; private String appointmentTime; private String appointmentStatus; @@ -25,9 +27,16 @@ public class AppointmentDTO { public AppointmentDTO(Long customerId, Long storeId, Long serviceId, String appointmentDate, String appointmentTime, String appointmentStatus, List petIds) { + this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petIds); + } + + public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId, + String appointmentDate, String appointmentTime, + String appointmentStatus, List petIds) { this.customerId = customerId; this.storeId = storeId; this.serviceId = serviceId; + this.employeeId = employeeId; this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; @@ -63,6 +72,14 @@ public class AppointmentDTO { return serviceName; } + public Long getEmployeeId() { + return employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + public String getAppointmentDate() { return appointmentDate; } diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 56e53a56..5c56862d 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -2,6 +2,8 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.DropdownOption; import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.*; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -25,12 +27,15 @@ public class DropdownController { private final CategoryRepository categoryRepository; private final StoreRepository storeRepository; private final SupplierRepository supplierRepository; + private final EmployeeStoreRepository employeeStoreRepository; + private final UserRepository userRepository; public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, CustomerPetRepository customerPetRepository, ServiceRepository serviceRepository, ProductRepository productRepository, CategoryRepository categoryRepository, StoreRepository storeRepository, - SupplierRepository supplierRepository) { + SupplierRepository supplierRepository, EmployeeStoreRepository employeeStoreRepository, + UserRepository userRepository) { this.petRepository = petRepository; this.customerRepository = customerRepository; this.customerPetRepository = customerPetRepository; @@ -39,6 +44,8 @@ public class DropdownController { this.categoryRepository = categoryRepository; this.storeRepository = storeRepository; this.supplierRepository = supplierRepository; + this.employeeStoreRepository = employeeStoreRepository; + this.userRepository = userRepository; } @GetMapping("/pets") @@ -129,6 +136,17 @@ public class DropdownController { ); } + @GetMapping("/stores/{storeId}/employees") + @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + public ResponseEntity> getStoreEmployees(@PathVariable Long storeId) { + return ResponseEntity.ok( + employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(this::isAssignableEmployee) + .map(this::toEmployeeOption) + .collect(Collectors.toList()) + ); + } + @GetMapping("/suppliers") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity> getSuppliers() { @@ -144,4 +162,20 @@ public class DropdownController { String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " · " + pet.getBreed(); return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")"); } + + private DropdownOption toEmployeeOption(EmployeeStore employeeStore) { + var employee = employeeStore.getEmployee(); + return new DropdownOption(employee.getEmployeeId(), employee.getFirstName() + " " + employee.getLastName()); + } + + private boolean isAssignableEmployee(EmployeeStore employeeStore) { + Long userId = employeeStore.getEmployee().getUserId(); + if (userId == null) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java index 1807f5b4..9a34dff8 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -18,6 +18,8 @@ public class AdoptionRequest { @NotBlank(message = "Adoption status is required") private String adoptionStatus; + private Long employeeId; + public Long getPetId() { return petId; } @@ -50,6 +52,14 @@ public class AdoptionRequest { this.adoptionStatus = adoptionStatus; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -58,12 +68,13 @@ public class AdoptionRequest { return Objects.equals(petId, that.petId) && Objects.equals(customerId, that.customerId) && Objects.equals(adoptionDate, that.adoptionDate) && - Objects.equals(adoptionStatus, that.adoptionStatus); + Objects.equals(adoptionStatus, that.adoptionStatus) && + Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(petId, customerId, adoptionDate, adoptionStatus); + return Objects.hash(petId, customerId, adoptionDate, adoptionStatus, employeeId); } @Override @@ -73,6 +84,7 @@ public class AdoptionRequest { ", customerId=" + customerId + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + + ", employeeId=" + employeeId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java index 6f2d0556..43128d23 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java @@ -11,6 +11,8 @@ public class AdoptionResponse { private String petName; private Long customerId; private String customerName; + private Long employeeId; + private String employeeName; private LocalDate adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; @@ -20,12 +22,14 @@ public class AdoptionResponse { public AdoptionResponse() { } - public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { + public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, Long employeeId, String employeeName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.petId = petId; this.petName = petName; this.customerId = customerId; this.customerName = customerName; + this.employeeId = employeeId; + this.employeeName = employeeName; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; this.adoptionFee = adoptionFee; @@ -73,6 +77,22 @@ public class AdoptionResponse { this.customerName = customerName; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 8423d090..3d127c19 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -29,6 +29,8 @@ public class AppointmentRequest { private List customerPetIds; + private Long employeeId; + public Long getCustomerId() { return customerId; } @@ -93,6 +95,14 @@ public class AppointmentRequest { this.customerPetIds = customerPetIds; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -105,12 +115,13 @@ public class AppointmentRequest { Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petIds, that.petIds) && - Objects.equals(customerPetIds, that.customerPetIds); + Objects.equals(customerPetIds, that.customerPetIds) && + Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds); + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds, employeeId); } @Override @@ -124,6 +135,7 @@ public class AppointmentRequest { ", appointmentStatus='" + appointmentStatus + '\'' + ", petIds=" + petIds + ", customerPetIds=" + customerPetIds + + ", employeeId=" + employeeId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java index f6398248..efc1c300 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -17,6 +17,8 @@ public class AppointmentResponse { private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; + private Long employeeId; + private String employeeName; private List petNames; private List petIds; private List customerPetNames; @@ -124,6 +126,22 @@ public class AppointmentResponse { this.appointmentStatus = appointmentStatus; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public List getPetNames() { return petNames; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Adoption.java b/backend/src/main/java/com/petshop/backend/entity/Adoption.java index 84912ba9..78360e2e 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Adoption.java +++ b/backend/src/main/java/com/petshop/backend/entity/Adoption.java @@ -25,6 +25,10 @@ public class Adoption { @JoinColumn(name = "customerId", nullable = false) private Customer customer; + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + @Column(nullable = false) private LocalDate adoptionDate; @@ -42,10 +46,11 @@ public class Adoption { public Adoption() { } - public Adoption(Long adoptionId, Pet pet, Customer customer, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Adoption(Long adoptionId, Pet pet, Customer customer, Employee employee, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.pet = pet; this.customer = customer; + this.employee = employee; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; this.createdAt = createdAt; @@ -76,6 +81,14 @@ public class Adoption { this.customer = customer; } + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + public LocalDate getAdoptionDate() { return adoptionDate; } @@ -127,6 +140,7 @@ public class Adoption { "adoptionId=" + adoptionId + ", pet=" + pet + ", customer=" + customer + + ", employee=" + employee + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + ", createdAt=" + createdAt + diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java index cec10224..d4ebc199 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Appointment.java +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -31,6 +31,10 @@ public class Appointment { @JoinColumn(name = "serviceId", nullable = false) private Service service; + @ManyToOne + @JoinColumn(name = "employeeId", nullable = false) + private Employee employee; + @Column(nullable = false) private LocalDate appointmentDate; @@ -67,11 +71,12 @@ public class Appointment { public Appointment() { } - public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, Employee employee, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { this.appointmentId = appointmentId; this.customer = customer; this.store = store; this.service = service; + this.employee = employee; this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; @@ -112,6 +117,14 @@ public class Appointment { this.service = service; } + public Employee getEmployee() { + return employee; + } + + public void setEmployee(Employee employee) { + this.employee = employee; + } + public LocalDate getAppointmentDate() { return appointmentDate; } @@ -189,6 +202,7 @@ public class Appointment { ", customer=" + customer + ", store=" + store + ", service=" + service + + ", employee=" + employee + ", appointmentDate=" + appointmentDate + ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java index cfbf715f..e320fc00 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java @@ -15,6 +15,8 @@ import java.util.Optional; public interface EmployeeRepository extends JpaRepository { Optional findByUserId(Long userId); List findAllByEmail(String email); + Optional findFirstByIsActiveTrueOrderByEmployeeIdAsc(); + List findAllByIsActiveTrueOrderByEmployeeIdAsc(); @Query("SELECT e FROM Employee e WHERE " + "LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java index 1c847817..0cc3f771 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java @@ -2,11 +2,17 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.EmployeeStore; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface EmployeeStoreRepository extends JpaRepository { Optional findByEmployeeEmployeeId(Long employeeId); + + @Query("SELECT es FROM EmployeeStore es WHERE es.store.storeId = :storeId AND es.employee.isActive = true ORDER BY es.employee.employeeId ASC") + List findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index a8f7f476..4bda85ab 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -5,11 +5,15 @@ import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -21,11 +25,15 @@ public class AdoptionService { private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; private final CustomerRepository customerRepository; + private final EmployeeRepository employeeRepository; + private final UserRepository userRepository; - public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository) { + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository, EmployeeRepository employeeRepository, UserRepository userRepository) { this.adoptionRepository = adoptionRepository; this.petRepository = petRepository; this.customerRepository = customerRepository; + this.employeeRepository = employeeRepository; + this.userRepository = userRepository; } public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { @@ -66,10 +74,12 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); Adoption adoption = new Adoption(); adoption.setPet(pet); adoption.setCustomer(customer); + adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(request.getAdoptionStatus()); @@ -87,9 +97,11 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); adoption.setPet(pet); adoption.setCustomer(customer); + adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(request.getAdoptionStatus()); @@ -117,6 +129,8 @@ public class AdoptionService { adoption.getPet().getPetName(), adoption.getCustomer().getCustomerId(), adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), + adoption.getEmployee().getEmployeeId(), + adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(), adoption.getAdoptionDate(), adoption.getAdoptionStatus(), adoption.getPet().getPetPrice(), @@ -124,4 +138,31 @@ public class AdoptionService { adoption.getUpdatedAt() ); } + + private Employee resolveAdoptionEmployee(Long requestedEmployeeId) { + if (requestedEmployeeId != null) { + Employee employee = employeeRepository.findById(requestedEmployeeId) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); + if (!isAssignableEmployee(employee)) { + throw new IllegalArgumentException("Selected employee is not assignable for adoption work"); + } + return employee; + } + + return employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc().stream() + .filter(this::isAssignableEmployee) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is available for adoption assignment")); + } + + private boolean isAssignableEmployee(Employee employee) { + Long userId = employee.getUserId(); + if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index c5b615d3..0b277889 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -121,6 +121,7 @@ public class AppointmentService { Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); appointment.setCustomer(customer); @@ -131,6 +132,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -165,6 +167,7 @@ public class AppointmentService { Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); appointment.setStore(store); @@ -174,6 +177,7 @@ public class AppointmentService { appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); appointment.setCustomerPets(customerPets); + appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); return mapToResponse(appointment); @@ -289,6 +293,8 @@ public class AppointmentService { response.setAppointmentDate(appointment.getAppointmentDate()); response.setAppointmentTime(appointment.getAppointmentTime()); response.setAppointmentStatus(appointment.getAppointmentStatus()); + response.setEmployeeId(appointment.getEmployee().getEmployeeId()); + response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); response.setPetNames(petNames); response.setPetIds(petIds); response.setCustomerPetNames(customerPetNames); @@ -299,6 +305,39 @@ public class AppointmentService { return response; } + private Employee resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) { + List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(es -> isAssignableEmployee(es.getEmployee())) + .collect(Collectors.toList()); + + if (requestedEmployeeId != null) { + Employee employee = employeeRepository.findById(requestedEmployeeId) + .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); + boolean assignedToStore = assignableEmployees.stream() + .anyMatch(es -> es.getEmployee().getEmployeeId().equals(requestedEmployeeId)); + if (!assignedToStore) { + throw new IllegalArgumentException("Selected employee is not assignable for the selected store"); + } + return employee; + } + + return assignableEmployees.stream() + .map(EmployeeStore::getEmployee) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is assigned to the selected store")); + } + + private boolean isAssignableEmployee(Employee employee) { + Long userId = employee.getUserId(); + if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { + return false; + } + return userRepository.findById(userId) + .map(User::getRole) + .filter(role -> role == User.Role.STAFF) + .isPresent(); + } + //------------------------------------ private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { // Filter by same service only - different services can run at same time diff --git a/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql b/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql new file mode 100644 index 00000000..e931e32a --- /dev/null +++ b/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql @@ -0,0 +1,61 @@ +ALTER TABLE appointment + ADD COLUMN employeeId BIGINT NULL; + +UPDATE appointment a +SET a.employeeId = ( + SELECT es.employeeId + FROM employeeStore es + JOIN employee e ON e.employeeId = es.employeeId + JOIN users u ON u.id = e.user_id + WHERE es.storeId = a.storeId + AND e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY es.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +UPDATE appointment a +SET a.employeeId = ( + SELECT e.employeeId + FROM employee e + JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY e.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +ALTER TABLE appointment + ADD CONSTRAINT fk_appointment_employee + FOREIGN KEY (employeeId) REFERENCES employee(employeeId); + +CREATE INDEX idx_appointment_employeeId ON appointment(employeeId); + +ALTER TABLE appointment + MODIFY employeeId BIGINT NOT NULL; + +ALTER TABLE adoption + ADD COLUMN employeeId BIGINT NULL; + +UPDATE adoption a +SET a.employeeId = ( + SELECT e.employeeId + FROM employee e + JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE + AND u.role = 'STAFF' + ORDER BY e.employeeId ASC + LIMIT 1 +) +WHERE a.employeeId IS NULL; + +ALTER TABLE adoption + ADD CONSTRAINT fk_adoption_employee + FOREIGN KEY (employeeId) REFERENCES employee(employeeId); + +CREATE INDEX idx_adoption_employeeId ON adoption(employeeId); + +ALTER TABLE adoption + MODIFY employeeId BIGINT NOT NULL; diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java new file mode 100644 index 00000000..52b64692 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -0,0 +1,90 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.StoreLocation; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.CategoryRepository; +import com.petshop.backend.repository.CustomerPetRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeStoreRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.ProductRepository; +import com.petshop.backend.repository.ServiceRepository; +import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.repository.SupplierRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DropdownControllerTest { + + @Test + void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { + PetRepository petRepository = mock(PetRepository.class); + CustomerRepository customerRepository = mock(CustomerRepository.class); + CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); + ServiceRepository serviceRepository = mock(ServiceRepository.class); + ProductRepository productRepository = mock(ProductRepository.class); + CategoryRepository categoryRepository = mock(CategoryRepository.class); + StoreRepository storeRepository = mock(StoreRepository.class); + SupplierRepository supplierRepository = mock(SupplierRepository.class); + EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + DropdownController controller = new DropdownController( + petRepository, + customerRepository, + customerPetRepository, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + employeeStoreRepository, + userRepository + ); + + StoreLocation store = new StoreLocation(); + store.setStoreId(1L); + + Employee staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + Employee adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + + User adminUser = new User(); + adminUser.setId(8L); + adminUser.setRole(User.Role.ADMIN); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); + + var response = controller.getStoreEmployees(1L); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); + assertEquals("Alex Jones", response.getBody().get(0).getLabel()); + } +} diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java new file mode 100644 index 00000000..3523e918 --- /dev/null +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -0,0 +1,124 @@ +package com.petshop.backend.service; + +import com.petshop.backend.dto.adoption.AdoptionRequest; +import com.petshop.backend.entity.Adoption; +import com.petshop.backend.entity.Customer; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.repository.EmployeeRepository; +import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.quality.Strictness; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class AdoptionServiceTest { + + @Mock private AdoptionRepository adoptionRepository; + @Mock private PetRepository petRepository; + @Mock private CustomerRepository customerRepository; + @Mock private EmployeeRepository employeeRepository; + @Mock private UserRepository userRepository; + + @InjectMocks + private AdoptionService adoptionService; + + private Pet pet; + private Customer customer; + private Employee staffEmployee; + private Employee adminEmployee; + + @BeforeEach + void setUp() { + pet = new Pet(); + pet.setPetId(1L); + pet.setPetName("Buddy"); + + customer = new Customer(); + customer.setCustomerId(1L); + customer.setFirstName("Pat"); + customer.setLastName("Owner"); + + staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + + User adminUser = new User(); + adminUser.setId(8L); + adminUser.setRole(User.Role.ADMIN); + when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); + } + + @Test + void createAdoptionAutoAssignsFirstStaffEmployee() { + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); + when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { + Adoption adoption = invocation.getArgument(0); + adoption.setAdoptionId(10L); + return adoption; + }); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setAdoptionDate(LocalDate.now()); + request.setAdoptionStatus("Pending"); + + var response = adoptionService.createAdoption(request); + + assertEquals(7L, response.getEmployeeId()); + assertEquals("Alex Jones", response.getEmployeeName()); + } + + @Test + void createAdoptionRejectsAdminEmployeeSelection() { + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setEmployeeId(8L); + request.setAdoptionDate(LocalDate.now()); + request.setAdoptionStatus("Pending"); + + assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + } +} diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index e978fcde..91e414f6 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -3,6 +3,8 @@ package com.petshop.backend.service; import com.petshop.backend.entity.Appointment; import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.CustomerPet; +import com.petshop.backend.entity.Employee; +import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Service; import com.petshop.backend.entity.StoreLocation; @@ -22,7 +24,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.quality.Strictness; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -41,6 +45,7 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class AppointmentServiceTest { @Mock @@ -79,6 +84,7 @@ class AppointmentServiceTest { private Service nailTrim; private Pet pet; private CustomerPet customerPet; + private Employee employee; private LocalDate date; @BeforeEach @@ -111,6 +117,18 @@ class AppointmentServiceTest { customerPet.setPetName("Milo Jr"); customerPet.setCustomer(customer); + employee = new Employee(); + employee.setEmployeeId(7L); + employee.setUserId(7L); + employee.setFirstName("Alex"); + employee.setLastName("Jones"); + employee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + date = LocalDate.now().plusDays(1); } @@ -171,6 +189,8 @@ class AppointmentServiceTest { when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); @@ -210,6 +230,8 @@ class AppointmentServiceTest { when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); @@ -238,6 +260,8 @@ class AppointmentServiceTest { when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { @@ -259,6 +283,51 @@ class AppointmentServiceTest { assertEquals(99L, response.getAppointmentId()); assertEquals(1L, response.getCustomerId()); + assertEquals(7L, response.getEmployeeId()); + } + + @Test + void createAppointmentRejectsAdminEmployeeSelection() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + Employee adminEmployee = new Employee(); + adminEmployee.setEmployeeId(8L); + adminEmployee.setUserId(8L); + adminEmployee.setFirstName("Admin"); + adminEmployee.setLastName("Helper"); + adminEmployee.setIsActive(true); + + User adminLinkedUser = new User(); + adminLinkedUser.setId(8L); + adminLinkedUser.setRole(User.Role.ADMIN); + + when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setEmployeeId(8L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(11L)); + + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { @@ -269,6 +338,7 @@ class AppointmentServiceTest { appointment.setAppointmentStatus("Booked"); appointment.setService(service); appointment.setStore(storeLocation); + appointment.setEmployee(employee); appointment.setCustomer(customer); appointment.setPets(Set.of()); return appointment; diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java index f749b5b5..71b7005c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -15,6 +15,8 @@ public class AppointmentDTO { private SimpleIntegerProperty serviceId; private SimpleStringProperty serviceName; + private SimpleIntegerProperty employeeId; + private SimpleStringProperty employeeName; private SimpleStringProperty appointmentDate; private SimpleStringProperty appointmentTime; @@ -25,6 +27,8 @@ public class AppointmentDTO { int customerId, String customerName, int petId, String petName, int serviceId, String serviceName, + int employeeId, + String employeeName, String appointmentDate, String appointmentTime, String appointmentStatus) { @@ -36,6 +40,8 @@ public class AppointmentDTO { this.petName = new SimpleStringProperty(petName); this.serviceId = new SimpleIntegerProperty(serviceId); this.serviceName = new SimpleStringProperty(serviceName); + this.employeeId = new SimpleIntegerProperty(employeeId); + this.employeeName = new SimpleStringProperty(employeeName); this.appointmentDate = new SimpleStringProperty(appointmentDate); this.appointmentTime = new SimpleStringProperty(appointmentTime); this.appointmentStatus = new SimpleStringProperty(appointmentStatus); @@ -52,8 +58,10 @@ public class AppointmentDTO { public int getServiceId() { return serviceId.get(); } public String getServiceName() { return serviceName.get(); } + public int getEmployeeId() { return employeeId.get(); } + public String getEmployeeName() { return employeeName.get(); } public String getAppointmentDate() { return appointmentDate.get(); } public String getAppointmentTime() { return appointmentTime.get(); } public String getAppointmentStatus() { return appointmentStatus.get(); } -} \ No newline at end of file +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java index 5bea7090..830488f1 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; public class AdoptionRequest { private Long petId; private Long customerId; + private Long employeeId; private LocalDate adoptionDate; private String adoptionStatus; @@ -27,6 +28,14 @@ public class AdoptionRequest { this.customerId = customerId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java index 60667217..8d0d1093 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java @@ -6,8 +6,10 @@ public class AdoptionResponse { private Long adoptionId; private Long petId; private Long customerId; + private Long employeeId; private String petName; private String customerName; + private String employeeName; private LocalDate adoptionDate; private java.math.BigDecimal adoptionFee; private String adoptionStatus; @@ -39,6 +41,14 @@ public class AdoptionResponse { this.customerId = customerId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public String getPetName() { return petName; } @@ -55,6 +65,14 @@ public class AdoptionResponse { this.customerName = customerName; } + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java index 299fcae9..c87b2e87 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java @@ -10,6 +10,7 @@ public class AppointmentRequest { private Long customerId; private Long storeId; private Long serviceId; + private Long employeeId; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; @@ -57,6 +58,14 @@ public class AppointmentRequest { this.serviceId = serviceId; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + public LocalDate getAppointmentDate() { return appointmentDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java index c71dc3f3..d1768a94 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java @@ -15,6 +15,8 @@ public class AppointmentResponse { private java.util.List customerPetNames; private java.util.List customerPetIds; private String serviceName; + private Long employeeId; + private String employeeName; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; @@ -110,6 +112,22 @@ public class AppointmentResponse { this.serviceName = serviceName; } + public Long getEmployeeId() { + return employeeId; + } + + public void setEmployeeId(Long employeeId) { + this.employeeId = employeeId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + public LocalDate getAppointmentDate() { return appointmentDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index ec0fe4d0..fec5fae2 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -97,4 +97,12 @@ public class DropdownApi { } return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + + public List getStoreEmployees(Long storeId) throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/stores/" + storeId + "/employees"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from store employees endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 564e6205..0b8fe447 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -43,6 +43,9 @@ public class AdoptionController { @FXML private TableColumn colCustomerName; + @FXML + private TableColumn colEmployeeName; + @FXML private TableColumn colAdoptionDate; @@ -71,6 +74,7 @@ public class AdoptionController { colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); colPetId.setCellValueFactory(new PropertyValueFactory<>("petName")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); colAdoptionDate.setCellValueFactory(new PropertyValueFactory<>("adoptionDate")); colAdoptionFee.setCellValueFactory(new PropertyValueFactory<>("adoptionFee")); colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus")); @@ -252,8 +256,10 @@ public class AdoptionController { response.getAdoptionId().intValue(), response.getPetId() != null ? response.getPetId().intValue() : 0, response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, + response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0, response.getPetName(), response.getCustomerName(), + response.getEmployeeName(), response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "", response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0, response.getAdoptionStatus() diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index e310c039..ba4c05aa 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -33,6 +33,7 @@ public class AppointmentController { @FXML private TableColumn colAppointmentDate; @FXML private TableColumn colAppointmentTime; @FXML private TableColumn colCustomerName; + @FXML private TableColumn colEmployeeName; @FXML private TableColumn colAppointmentStatus; @FXML private Button btnAdd; @@ -55,6 +56,7 @@ public class AppointmentController { colAppointmentDate.setCellValueFactory(new PropertyValueFactory<>("appointmentDate")); colAppointmentTime.setCellValueFactory(new PropertyValueFactory<>("appointmentTime")); colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus")); filtered = new FilteredList<>(appointments, a -> true); @@ -247,6 +249,8 @@ public class AppointmentController { petName, response.getServiceId() != null ? response.getServiceId().intValue() : 0, response.getServiceName(), + response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0, + response.getEmployeeName(), response.getAppointmentDate().toString(), response.getAppointmentTime().toString(), response.getAppointmentStatus() diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index a1ed25fb..db7094c7 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -11,6 +11,7 @@ import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.DatePicker; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; @@ -38,6 +39,9 @@ public class AdoptionDialogController { @FXML private ComboBox cbCustomer; + @FXML + private ComboBox cbEmployee; + @FXML private ComboBox cbPet; @@ -62,6 +66,7 @@ public class AdoptionDialogController { void initialize() { cbAdoptionStatus.setItems(statusList); + cbEmployee.setPromptText("Select an employee"); new Thread(() -> { try { @@ -83,6 +88,38 @@ public class AdoptionDialogController { } }).start(); + new Thread(() -> { + try { + Long storeId = org.example.petshopdesktop.auth.UserSession.getInstance().getStoreId(); + List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); + Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.initialize", + e, + "Loading employees for combo box"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + + cbEmployee.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbEmployee.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + new Thread(() -> { try { List customers = DropdownApi.getInstance().getCustomers(); @@ -129,6 +166,10 @@ public class AdoptionDialogController { errorMsg += "Customer is required.\n"; } + if (cbEmployee.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Employee is required.\n"; + } + if (dpAdoptionDate.getValue() == null) { errorMsg += "Adoption Date is required.\n"; } @@ -142,6 +183,7 @@ public class AdoptionDialogController { AdoptionRequest request = new AdoptionRequest(); request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId()); request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId()); + request.setEmployeeId(cbEmployee.getSelectionModel().getSelectedItem().getId()); request.setAdoptionDate(dpAdoptionDate.getValue()); request.setAdoptionStatus(cbAdoptionStatus.getValue()); @@ -204,6 +246,15 @@ public class AdoptionDialogController { } } + if (adoption.getEmployeeId() > 0) { + for (DropdownOption employee : cbEmployee.getItems()) { + if (employee.getId() != null && employee.getId().equals(adoption.getEmployeeId())) { + cbEmployee.getSelectionModel().select(employee); + break; + } + } + } + if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { try { dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate())); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 81b54cb9..191e58ad 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -35,6 +35,7 @@ public class AppointmentDialogController { @FXML private ComboBox cbService; @FXML private ComboBox cbCustomer; @FXML private ComboBox cbPet; + @FXML private ComboBox cbEmployee; @FXML private ComboBox cbHour; @FXML private ComboBox cbMinute; @@ -94,14 +95,32 @@ public class AppointmentDialogController { ActivityLogger.getInstance().logException( "AppointmentDialogController.initialize", e, - "Loading combo box data for services, customers, and pets"); + "Loading services/customers for appointment dialog"); e.printStackTrace(); }); } }).start(); + new Thread(() -> { + try { + Long storeId = UserSession.getInstance().getStoreId(); + List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); + Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.initialize", + e, + "Loading employees for appointment dialog"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + cbAppointmentStatus.setItems(statusList); cbPet.setDisable(true); + cbEmployee.setPromptText("Select an employee"); cbPet.setPromptText("Select a customer first"); cbCustomer.setPromptText("Select a customer"); cbService.setPromptText("Select a service"); @@ -161,6 +180,21 @@ public class AppointmentDialogController { } }); + cbEmployee.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbEmployee.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbCustomer.valueProperty().addListener((obs, oldValue, newValue) -> { Long customerId = newValue != null ? newValue.getId() : null; cbPet.setValue(null); @@ -222,6 +256,14 @@ public class AppointmentDialogController { cbCustomer.setValue(c); } }); + + if (appt.getEmployeeId() > 0) { + cbEmployee.getItems().forEach(employee -> { + if (employee.getId() != null && employee.getId().longValue() == appt.getEmployeeId()) { + cbEmployee.setValue(employee); + } + }); + } } // @@ -233,6 +275,7 @@ public class AppointmentDialogController { if (cbService.getValue() == null || cbCustomer.getValue() == null || cbPet.getValue() == null || + cbEmployee.getValue() == null || dpAppointmentDate.getValue() == null || cbHour.getValue() == null || cbMinute.getValue() == null || @@ -254,6 +297,7 @@ public class AppointmentDialogController { request.setCustomerId(cbCustomer.getValue().getId()); request.setStoreId(storeId); request.setServiceId(cbService.getValue().getId()); + request.setEmployeeId(cbEmployee.getValue().getId()); request.setAppointmentDate(dpAppointmentDate.getValue()); request.setAppointmentTime(appointmentTime); request.setAppointmentStatus(cbAppointmentStatus.getValue()); diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java index c5c02f63..98d1f4a6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java @@ -8,18 +8,22 @@ public class Adoption { private SimpleIntegerProperty adoptionId; private SimpleIntegerProperty petId; private SimpleIntegerProperty customerId; + private SimpleIntegerProperty employeeId; private SimpleStringProperty petName; private SimpleStringProperty customerName; + private SimpleStringProperty employeeName; private SimpleStringProperty adoptionDate; private SimpleDoubleProperty adoptionFee; private SimpleStringProperty adoptionStatus; - public Adoption(int adoptionId, int petId, int customerId, String petName, String customerName, String adoptionDate, double adoptionFee, String adoptionStatus) { + public Adoption(int adoptionId, int petId, int customerId, int employeeId, String petName, String customerName, String employeeName, String adoptionDate, double adoptionFee, String adoptionStatus) { this.adoptionId = new SimpleIntegerProperty(adoptionId); this.petId = new SimpleIntegerProperty(petId); this.customerId = new SimpleIntegerProperty(customerId); + this.employeeId = new SimpleIntegerProperty(employeeId); this.petName = new SimpleStringProperty(petName); this.customerName = new SimpleStringProperty(customerName); + this.employeeName = new SimpleStringProperty(employeeName); this.adoptionDate = new SimpleStringProperty(adoptionDate); this.adoptionFee = new SimpleDoubleProperty(adoptionFee); this.adoptionStatus = new SimpleStringProperty(adoptionStatus); @@ -43,6 +47,12 @@ public class Adoption { public SimpleIntegerProperty customerIdProperty() { return customerId; } + public int getEmployeeId() { return employeeId.get(); } + + public void setEmployeeId(int employeeId) { this.employeeId.set(employeeId); } + + public SimpleIntegerProperty employeeIdProperty() { return employeeId; } + public String getPetName() { return petName.get(); } public void setPetName(String petName) { this.petName.set(petName); } @@ -55,6 +65,12 @@ public class Adoption { public SimpleStringProperty customerNameProperty() { return customerName; } + public String getEmployeeName() { return employeeName.get(); } + + public void setEmployeeName(String employeeName) { this.employeeName.set(employeeName); } + + public SimpleStringProperty employeeNameProperty() { return employeeName; } + public String getAdoptionDate() { return adoptionDate.get(); } public void setAdoptionDate(String adoptionDate) { this.adoptionDate.set(adoptionDate); } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml index c3a14b62..b1140dad 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml @@ -73,6 +73,7 @@ + @@ -131,6 +132,20 @@ + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml index 99cf0833..f3cccdac 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml @@ -83,6 +83,7 @@ + @@ -190,9 +191,9 @@ - - - + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index 9443ef14..896ed71e 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -68,11 +68,12 @@ - - - - - + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml index f698c5c1..8e920be0 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -68,12 +68,13 @@ - - - - - - + + + + + + + diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 4643bf0e..f8e4ec2d 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -203,6 +203,7 @@ function AppointmentsPage() { const didPreselectRef = useRef(false); const [stores, setStores] = useState([]); + const [employees, setEmployees] = useState([]); const [services, setServices] = useState([]); const [allPets, setAllPets] = useState([]); const [customerPets, setCustomerPets] = useState([]); @@ -210,6 +211,7 @@ function AppointmentsPage() { const [storeId, setStoreId] = useState(""); const [serviceId, setServiceId] = useState(""); + const [employeeId, setEmployeeId] = useState(""); const [appointmentDate, setAppointmentDate] = useState(""); const [appointmentTime, setAppointmentTime] = useState(""); const [selectedPetIds, setSelectedPetIds] = useState([]); @@ -302,6 +304,33 @@ function AppointmentsPage() { loadAppointments(); }, [loadAppointments]); + useEffect(() => { + if (!token || !storeId) { + setEmployees([]); + setEmployeeId(""); + return; + } + + fetch(`${API_BASE}/api/v1/dropdowns/stores/${storeId}/employees`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((r) => r.json()) + .then((data) => setEmployees(Array.isArray(data) ? data : [])) + .catch(() => setEmployees([])); + }, [token, storeId]); + + useEffect(() => { + if (!employees.length) { + setEmployeeId(""); + return; + } + + const currentExists = employees.some((employee) => String(employee.id) === String(employeeId)); + if (!currentExists) { + setEmployeeId(String(employees[0].id)); + } + }, [employees, employeeId]); + useEffect(() => { if (!storeId || !serviceId || !appointmentDate) { setAvailableSlots([]); @@ -401,6 +430,7 @@ function AppointmentsPage() { customerId: user.customerId, storeId: Number(storeId), serviceId: Number(serviceId), + employeeId: employeeId ? Number(employeeId) : undefined, appointmentDate, appointmentTime: appointmentTime + ":00", appointmentStatus: "Booked", @@ -513,6 +543,21 @@ function AppointmentsPage() { + {employees.length > 0 && ( + + )} + {selectedService && (

{selectedService.serviceDesc}

From 1f343f413298086cbddccb54f8fd37b51cbf3f52 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 15:51:11 -0600 Subject: [PATCH 055/137] Harden assignment rules --- ...calAppointmentCustomerSeedInitializer.java | 34 +++ .../controller/AdoptionController.java | 15 +- .../controller/DropdownController.java | 24 ++- .../repository/AdoptionRepository.java | 4 + .../repository/CustomerRepository.java | 3 + .../backend/repository/PetRepository.java | 4 + .../backend/service/AdoptionService.java | 61 +++++- .../backend/service/AppointmentService.java | 4 +- .../resources/dev/seed_demo_customer_pets.sql | 53 +++++ .../controller/DropdownControllerTest.java | 98 +++++++++ .../backend/service/AdoptionServiceTest.java | 25 +++ .../service/AppointmentServiceTest.java | 43 ++++ .../api/endpoints/DropdownApi.java | 16 ++ .../AdoptionDialogController.java | 83 +++++--- .../AppointmentDialogController.java | 199 +++++++++++------- 15 files changed, 546 insertions(+), 120 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java create mode 100644 backend/src/main/resources/dev/seed_demo_customer_pets.sql diff --git a/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java b/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java new file mode 100644 index 00000000..36b78fb4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java @@ -0,0 +1,34 @@ +package com.petshop.backend.config; + +import com.petshop.backend.repository.CustomerPetRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; + +@Component +@Profile("local") +public class LocalAppointmentCustomerSeedInitializer implements CommandLineRunner { + + private final DataSource dataSource; + private final CustomerPetRepository customerPetRepository; + + public LocalAppointmentCustomerSeedInitializer(DataSource dataSource, CustomerPetRepository customerPetRepository) { + this.dataSource = dataSource; + this.customerPetRepository = customerPetRepository; + } + + @Override + public void run(String... args) { + if (customerPetRepository.count() > 0) { + return; + } + + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8", + new ClassPathResource("dev/seed_demo_customer_pets.sql")); + populator.execute(dataSource); + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index a3f67002..41a2e815 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -71,21 +71,8 @@ public class AdoptionController { } @PostMapping - @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createAdoption(@Valid @RequestBody AdoptionRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - if (!request.getCustomerId().equals(customer.getCustomerId())) { - throw new org.springframework.security.access.AccessDeniedException("You can only create adoptions for yourself"); - } - } - return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 5c56862d..409891bc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -57,6 +57,16 @@ public class DropdownController { ); } + @GetMapping("/adoption-pets") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getAdoptionPets() { + return ResponseEntity.ok( + petRepository.findAllByPetStatusIgnoreCaseOrderByPetNameAsc("Available").stream() + .map(p -> new DropdownOption(p.getPetId(), p.getPetName())) + .collect(Collectors.toList()) + ); + } + @GetMapping("/customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getCustomers() { @@ -67,6 +77,16 @@ public class DropdownController { ); } + @GetMapping("/appointment-customers") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") + public ResponseEntity> getAppointmentCustomers() { + return ResponseEntity.ok( + customerRepository.findAllWithPets().stream() + .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) + .collect(Collectors.toList()) + ); + } + @GetMapping("/customers/{customerId}/pets") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getCustomerPets(@PathVariable Long customerId) { @@ -174,8 +194,8 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } } diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 2af9c52f..7b632f7f 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -28,4 +28,8 @@ public interface AdoptionRepository extends JpaRepository { Page searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); + + boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); + + boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); } diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java index 56e03dbc..2c860de7 100644 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java @@ -16,6 +16,9 @@ public interface CustomerRepository extends JpaRepository { Optional findByUserId(Long userId); List findAllByEmail(String email); + + @Query("SELECT DISTINCT c FROM Customer c WHERE EXISTS (SELECT cp FROM CustomerPet cp WHERE cp.customer = c) ORDER BY c.firstName ASC, c.lastName ASC") + List findAllWithPets(); @Query("SELECT c FROM Customer c WHERE " + "LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index d974deb3..468c0c9d 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -8,9 +8,13 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface PetRepository extends JpaRepository { + List findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus); + @Query("SELECT p FROM Pet p WHERE " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 4bda85ab..c6ff1c60 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -22,6 +22,12 @@ import org.springframework.transaction.annotation.Transactional; @Service public class AdoptionService { + private static final String ADOPTION_STATUS_PENDING = "Pending"; + private static final String ADOPTION_STATUS_COMPLETED = "Completed"; + private static final String ADOPTION_STATUS_CANCELLED = "Cancelled"; + private static final String PET_STATUS_AVAILABLE = "Available"; + private static final String PET_STATUS_ADOPTED = "Adopted"; + private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; private final CustomerRepository customerRepository; @@ -75,15 +81,18 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); + validatePetAvailability(pet, null); Adoption adoption = new Adoption(); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); - adoption.setAdoptionStatus(request.getAdoptionStatus()); + adoption.setAdoptionStatus(adoptionStatus); adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId()); return mapToResponse(adoption); } @@ -98,14 +107,17 @@ public class AdoptionService { Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); + validatePetAvailability(pet, adoption.getAdoptionId()); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); adoption.setAdoptionDate(request.getAdoptionDate()); - adoption.setAdoptionStatus(request.getAdoptionStatus()); + adoption.setAdoptionStatus(adoptionStatus); adoption = adoptionRepository.save(adoption); + syncPetStatus(pet, adoptionStatus, adoption.getAdoptionId()); return mapToResponse(adoption); } @@ -161,8 +173,49 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } + + private String normalizeAdoptionStatus(String adoptionStatus) { + if (adoptionStatus == null) { + throw new IllegalArgumentException("Adoption status is required"); + } + String trimmedStatus = adoptionStatus.trim(); + if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_PENDING; + } + if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_COMPLETED; + } + if (ADOPTION_STATUS_CANCELLED.equalsIgnoreCase(trimmedStatus)) { + return ADOPTION_STATUS_CANCELLED; + } + throw new IllegalArgumentException("Adoption status must be Pending, Completed, or Cancelled"); + } + + private void validatePetAvailability(Pet pet, Long adoptionId) { + boolean adoptedElsewhere = adoptionId == null + ? adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) + : adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + if (adoptedElsewhere) { + throw new IllegalArgumentException("Selected pet has already been adopted"); + } + + if (!PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus()) && adoptionId == null) { + throw new IllegalArgumentException("Selected pet is not available for adoption"); + } + } + + private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) { + boolean completedElsewhere = adoptionId != null + && adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { + pet.setPetStatus(PET_STATUS_ADOPTED); + } else { + pet.setPetStatus(PET_STATUS_AVAILABLE); + } + petRepository.save(pet); + } } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 0b277889..3b3d121d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -333,8 +333,8 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .map(User::getRole) - .filter(role -> role == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/resources/dev/seed_demo_customer_pets.sql b/backend/src/main/resources/dev/seed_demo_customer_pets.sql new file mode 100644 index 00000000..18a855ab --- /dev/null +++ b/backend/src/main/resources/dev/seed_demo_customer_pets.sql @@ -0,0 +1,53 @@ +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Rocky', 'dog', 'Labrador', NULL +FROM customer c +WHERE c.email = 'alex@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Rocky' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Whiskers', 'cat', 'Persian', NULL +FROM customer c +WHERE c.email = 'emily@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Whiskers' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Daisy', 'dog', 'Beagle', NULL +FROM customer c +WHERE c.email = 'james@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Daisy' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Pepper', 'cat', 'Domestic Shorthair', NULL +FROM customer c +WHERE c.email = 'olivia@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Pepper' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Cooper', 'dog', 'Golden Retriever', NULL +FROM customer c +WHERE c.email = 'william@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Cooper' + ); + +INSERT INTO customer_pet (customer_id, pet_name, species, breed, image_url) +SELECT c.customerId, 'Mittens', 'cat', 'Siamese', NULL +FROM customer c +WHERE c.email = 'sophia@gmail.com' + AND NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = c.customerId AND cp.pet_name = 'Mittens' + ); diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index 52b64692..b7d97e74 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -2,6 +2,7 @@ package com.petshop.backend.controller; import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.CategoryRepository; @@ -71,10 +72,12 @@ class DropdownControllerTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); User adminUser = new User(); adminUser.setId(8L); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store))); @@ -87,4 +90,99 @@ class DropdownControllerTest { assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); assertEquals("Alex Jones", response.getBody().get(0).getLabel()); } + + @Test + void getStoreEmployeesExcludesInactiveStaffUsers() { + PetRepository petRepository = mock(PetRepository.class); + CustomerRepository customerRepository = mock(CustomerRepository.class); + CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); + ServiceRepository serviceRepository = mock(ServiceRepository.class); + ProductRepository productRepository = mock(ProductRepository.class); + CategoryRepository categoryRepository = mock(CategoryRepository.class); + StoreRepository storeRepository = mock(StoreRepository.class); + SupplierRepository supplierRepository = mock(SupplierRepository.class); + EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + DropdownController controller = new DropdownController( + petRepository, + customerRepository, + customerPetRepository, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + employeeStoreRepository, + userRepository + ); + + StoreLocation store = new StoreLocation(); + store.setStoreId(1L); + + Employee inactiveStaffEmployee = new Employee(); + inactiveStaffEmployee.setEmployeeId(7L); + inactiveStaffEmployee.setUserId(7L); + inactiveStaffEmployee.setFirstName("Alex"); + inactiveStaffEmployee.setLastName("Jones"); + inactiveStaffEmployee.setIsActive(true); + + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(inactiveStaffEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + var response = controller.getStoreEmployees(1L); + + assertEquals(0, response.getBody().size()); + } + + @Test + void getAppointmentCustomersReturnsOnlyCustomersWithPets() { + PetRepository petRepository = mock(PetRepository.class); + CustomerRepository customerRepository = mock(CustomerRepository.class); + CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); + ServiceRepository serviceRepository = mock(ServiceRepository.class); + ProductRepository productRepository = mock(ProductRepository.class); + CategoryRepository categoryRepository = mock(CategoryRepository.class); + StoreRepository storeRepository = mock(StoreRepository.class); + SupplierRepository supplierRepository = mock(SupplierRepository.class); + EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); + UserRepository userRepository = mock(UserRepository.class); + + DropdownController controller = new DropdownController( + petRepository, + customerRepository, + customerPetRepository, + serviceRepository, + productRepository, + categoryRepository, + storeRepository, + supplierRepository, + employeeStoreRepository, + userRepository + ); + + Customer one = new Customer(); + one.setCustomerId(1L); + one.setFirstName("Alex"); + one.setLastName("Brown"); + + Customer two = new Customer(); + two.setCustomerId(2L); + two.setFirstName("Emily"); + two.setLastName("Clark"); + + when(customerRepository.findAllWithPets()).thenReturn(List.of(one, two)); + + var response = controller.getAppointmentCustomers(); + + assertEquals(2, response.getBody().size()); + assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); + assertEquals("Alex Brown", response.getBody().get(0).getLabel()); + } } diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 3523e918..6192ccb2 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -52,6 +52,7 @@ class AdoptionServiceTest { pet = new Pet(); pet.setPetId(1L); pet.setPetName("Buddy"); + pet.setPetStatus("Available"); customer = new Customer(); customer.setCustomerId(1L); @@ -75,11 +76,13 @@ class AdoptionServiceTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); User adminUser = new User(); adminUser.setId(8L); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); } @@ -121,4 +124,26 @@ class AdoptionServiceTest { assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); } + + @Test + void createAdoptionRejectsInactiveStaffUserSelection() { + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(employeeRepository.findById(7L)).thenReturn(Optional.of(staffEmployee)); + + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(1L); + request.setCustomerId(1L); + request.setEmployeeId(7L); + request.setAdoptionDate(LocalDate.now()); + request.setAdoptionStatus("Pending"); + + assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + } } diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 91e414f6..9d9daa40 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -127,6 +127,7 @@ class AppointmentServiceTest { User staffUser = new User(); staffUser.setId(7L); staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); date = LocalDate.now().plusDays(1); @@ -215,6 +216,7 @@ class AppointmentServiceTest { adminUser.setId(99L); adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); setAuthentication(99L, User.Role.ADMIN); @@ -253,6 +255,7 @@ class AppointmentServiceTest { adminUser.setId(99L); adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); setAuthentication(99L, User.Role.ADMIN); @@ -306,6 +309,7 @@ class AppointmentServiceTest { User adminLinkedUser = new User(); adminLinkedUser.setId(8L); adminLinkedUser.setRole(User.Role.ADMIN); + adminLinkedUser.setActive(true); when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); @@ -330,6 +334,45 @@ class AppointmentServiceTest { assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } + @Test + void createAppointmentRejectsInactiveStaffUserSelection() { + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setUsername("admin"); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + adminUser.setTokenVersion(0); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + setAuthentication(99L, User.Role.ADMIN); + + User inactiveStaffUser = new User(); + inactiveStaffUser.setId(7L); + inactiveStaffUser.setRole(User.Role.STAFF); + inactiveStaffUser.setActive(false); + when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee)); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setEmployeeId(7L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(11L)); + + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + } + private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { Appointment appointment = new Appointment(); appointment.setAppointmentId(id); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index fec5fae2..05b2785c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -74,6 +74,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getAppointmentCustomers() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/appointment-customers"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from appointment customers endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getPets() throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/pets"); if (response == null || response.isEmpty()) { @@ -82,6 +90,14 @@ public class DropdownApi { return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + public List getAdoptionPets() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/adoption-pets"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from adoption pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + public List getCustomerPets(Long customerId) throws Exception { String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets"); if (response == null || response.isEmpty()) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index db7094c7..3a331711 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -18,11 +18,13 @@ import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.endpoints.AdoptionApi; import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.Adoption; import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalDate; import java.util.List; +import java.util.Objects; public class AdoptionDialogController { @@ -56,6 +58,7 @@ public class AdoptionDialogController { //Stores if the dialog view is in add/edit mode private String mode = null; + private Adoption selectedAdoption = null; //Adoption statuses private ObservableList statusList = FXCollections.observableArrayList( @@ -70,11 +73,12 @@ public class AdoptionDialogController { new Thread(() -> { try { - List pets = DropdownApi.getInstance().getPets(); + List pets = DropdownApi.getInstance().getAdoptionPets(); Platform.runLater(() -> { if (pets != null) { ObservableList petsObs = FXCollections.observableArrayList(pets); cbPet.setItems(petsObs); + applySelectedPet(); } }); } catch (Exception e) { @@ -90,9 +94,12 @@ public class AdoptionDialogController { new Thread(() -> { try { - Long storeId = org.example.petshopdesktop.auth.UserSession.getInstance().getStoreId(); + Long storeId = UserSession.getInstance().getStoreId(); List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); - Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); + Platform.runLater(() -> { + cbEmployee.setItems(FXCollections.observableArrayList(employees)); + applySelectedEmployee(); + }); } catch (Exception e) { Platform.runLater(() -> { ActivityLogger.getInstance().logException( @@ -127,6 +134,7 @@ public class AdoptionDialogController { if (customers != null) { ObservableList customersObs = FXCollections.observableArrayList(customers); cbCustomer.setItems(customersObs); + applySelectedCustomer(); } }); } catch (Exception e) { @@ -230,30 +238,11 @@ public class AdoptionDialogController { public void displayAdoptionDetails(Adoption adoption) { if (adoption != null) { + selectedAdoption = adoption; lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); - - for (DropdownOption pet : cbPet.getItems()) { - if (pet.getLabel().equals(adoption.getPetName())) { - cbPet.getSelectionModel().select(pet); - break; - } - } - - for (DropdownOption customer : cbCustomer.getItems()) { - if (customer.getLabel().equals(adoption.getCustomerName())) { - cbCustomer.getSelectionModel().select(customer); - break; - } - } - - if (adoption.getEmployeeId() > 0) { - for (DropdownOption employee : cbEmployee.getItems()) { - if (employee.getId() != null && employee.getId().equals(adoption.getEmployeeId())) { - cbEmployee.getSelectionModel().select(employee); - break; - } - } - } + applySelectedPet(); + applySelectedCustomer(); + applySelectedEmployee(); if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { try { @@ -280,4 +269,46 @@ public class AdoptionDialogController { lblMode.setText(mode + " Adoption"); lblAdoptionId.setVisible(mode.equals("Edit")); } + + private void applySelectedPet() { + if (selectedAdoption == null || selectedAdoption.getPetId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbPet.getItems(), (long) selectedAdoption.getPetId()); + if (selected != null && !Objects.equals(cbPet.getValue(), selected)) { + cbPet.setValue(selected); + } + } + + private void applySelectedCustomer() { + if (selectedAdoption == null || selectedAdoption.getCustomerId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAdoption.getCustomerId()); + if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) { + cbCustomer.setValue(selected); + } + } + + private void applySelectedEmployee() { + if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAdoption.getEmployeeId()); + if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) { + cbEmployee.setValue(selected); + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) { + return null; + } + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) { + return option; + } + } + return null; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 191e58ad..472ca7b4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -20,8 +20,9 @@ import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalTime; import java.time.LocalDate; -import java.util.Collections; import java.util.List; +import java.util.Collections; +import java.util.Objects; public class AppointmentDialogController { @@ -75,49 +76,6 @@ public class AppointmentDialogController { @FXML public void initialize() { - - new Thread(() -> { - try { - List services = DropdownApi.getInstance().getServices(); - List customers = DropdownApi.getInstance().getCustomers(); - - Platform.runLater(() -> { - if (services != null) { - cbService.setItems(FXCollections.observableArrayList(services)); - } - if (customers != null) { - cbCustomer.setItems(FXCollections.observableArrayList(customers)); - } - syncSelectedAppointment(); - }); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AppointmentDialogController.initialize", - e, - "Loading services/customers for appointment dialog"); - e.printStackTrace(); - }); - } - }).start(); - - new Thread(() -> { - try { - Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); - Platform.runLater(() -> cbEmployee.setItems(FXCollections.observableArrayList(employees))); - } catch (Exception e) { - Platform.runLater(() -> { - ActivityLogger.getInstance().logException( - "AppointmentDialogController.initialize", - e, - "Loading employees for appointment dialog"); - cbEmployee.setDisable(true); - cbEmployee.setPromptText("Unable to load employees"); - }); - } - }).start(); - cbAppointmentStatus.setItems(statusList); cbPet.setDisable(true); cbEmployee.setPromptText("Select an employee"); @@ -211,6 +169,10 @@ public class AppointmentDialogController { btnSave.setOnMouseClicked(this::buttonSaveClicked); btnCancel.setOnMouseClicked(this::closeStage); + + loadServices(); + loadAppointmentCustomers(); + loadEmployees(); } // @@ -221,6 +183,7 @@ public class AppointmentDialogController { selectedAppointment = appt; lblAppointmentId.setText("ID: " + appt.getAppointmentId()); + pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null; try { dpAppointmentDate.setValue( @@ -246,24 +209,9 @@ public class AppointmentDialogController { "Parsing appointment time"); } - cbService.getItems().forEach(s -> { - if (s.getId() != null && s.getId().longValue() == appt.getServiceId()) cbService.setValue(s); - }); - - cbCustomer.getItems().forEach(c -> { - if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) { - pendingPetSelectionId = (long) appt.getPetId(); - cbCustomer.setValue(c); - } - }); - - if (appt.getEmployeeId() > 0) { - cbEmployee.getItems().forEach(employee -> { - if (employee.getId() != null && employee.getId().longValue() == appt.getEmployeeId()) { - cbEmployee.setValue(employee); - } - }); - } + applySelectedService(); + applySelectedCustomer(); + applySelectedEmployee(); } // @@ -350,6 +298,49 @@ public class AppointmentDialogController { } } + private void applySelectedService() { + if (selectedAppointment == null || selectedAppointment.getServiceId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbService.getItems(), (long) selectedAppointment.getServiceId()); + if (selected != null && !Objects.equals(cbService.getValue(), selected)) { + cbService.setValue(selected); + } + } + + private void applySelectedCustomer() { + if (selectedAppointment == null || selectedAppointment.getCustomerId() <= 0) { + return; + } + + DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAppointment.getCustomerId()); + if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) { + cbCustomer.setValue(selected); + } + } + + private void applySelectedEmployee() { + if (selectedAppointment == null || selectedAppointment.getEmployeeId() <= 0) { + return; + } + DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAppointment.getEmployeeId()); + if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) { + cbEmployee.setValue(selected); + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) { + return null; + } + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) { + return option; + } + } + return null; + } + private void loadCustomerPets(Long customerId) { new Thread(() -> { try { @@ -359,22 +350,12 @@ public class AppointmentDialogController { cbPet.setDisable(pets == null || pets.isEmpty()); cbPet.setPromptText(pets == null || pets.isEmpty() ? "No pets for selected customer" : "Select a pet"); if (pendingPetSelectionId != null) { - boolean matched = false; for (DropdownOption pet : cbPet.getItems()) { if (pet.getId() != null && pet.getId().equals(pendingPetSelectionId)) { cbPet.setValue(pet); - matched = true; break; } } - if (!matched && selectedAppointment != null && selectedAppointment.getPetName() != null && !selectedAppointment.getPetName().isBlank()) { - DropdownOption legacy = new DropdownOption(); - legacy.setId(pendingPetSelectionId); - legacy.setLabel(selectedAppointment.getPetName() + " (legacy appointment pet)"); - cbPet.getItems().add(0, legacy); - cbPet.setValue(legacy); - cbPet.setDisable(false); - } pendingPetSelectionId = null; } }); @@ -391,4 +372,78 @@ public class AppointmentDialogController { } }).start(); } + + private void loadServices() { + new Thread(() -> { + try { + List services = DropdownApi.getInstance().getServices(); + Platform.runLater(() -> { + cbService.setItems(FXCollections.observableArrayList(services)); + applySelectedService(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadServices", + e, + "Loading services for appointment dialog"); + cbService.setDisable(true); + cbService.setPromptText("Unable to load services"); + }); + } + }).start(); + } + + private void loadAppointmentCustomers() { + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getAppointmentCustomers(); + Platform.runLater(() -> { + cbCustomer.setItems(FXCollections.observableArrayList(customers)); + boolean hasCustomers = customers != null && !customers.isEmpty(); + cbCustomer.setDisable(!hasCustomers); + cbPet.setDisable(true); + cbPet.setItems(FXCollections.observableArrayList()); + cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet"); + cbPet.setPromptText(hasCustomers ? "Select a customer first" : "No customer pets available"); + applySelectedCustomer(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadAppointmentCustomers", + e, + "Loading appointment customers for appointment dialog"); + cbCustomer.setDisable(true); + cbPet.setDisable(true); + cbCustomer.setPromptText("Unable to load customers"); + cbPet.setPromptText("Unable to load pets"); + }); + } + }).start(); + } + + private void loadEmployees() { + new Thread(() -> { + try { + Long storeId = UserSession.getInstance().getStoreId(); + List employees = storeId != null && storeId > 0 + ? DropdownApi.getInstance().getStoreEmployees(storeId) + : List.of(); + Platform.runLater(() -> { + cbEmployee.setItems(FXCollections.observableArrayList(employees)); + applySelectedEmployee(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.loadEmployees", + e, + "Loading employees for appointment dialog"); + cbEmployee.setDisable(true); + cbEmployee.setPromptText("Unable to load employees"); + }); + } + }).start(); + } } From 890391f9828c9bf93f2f9aa18c13e8852395eaa6 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:01:46 -0600 Subject: [PATCH 056/137] Allow admin ownership bypass --- .../controller/DropdownController.java | 11 +- .../backend/service/AdoptionService.java | 2 +- .../backend/service/AppointmentService.java | 21 +- .../controller/DropdownControllerTest.java | 142 +++++++------ .../backend/service/AdoptionServiceTest.java | 16 +- .../service/AppointmentServiceTest.java | 197 ++++++++---------- 6 files changed, 199 insertions(+), 190 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 409891bc..b307174c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -80,8 +80,15 @@ public class DropdownController { @GetMapping("/appointment-customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { + User user = com.petshop.backend.util.AuthenticationHelper.getAuthenticatedUser(userRepository); + List customers; + if (user.getRole() == User.Role.ADMIN) { + customers = customerRepository.findAll(); + } else { + customers = customerRepository.findAllWithPets(); + } return ResponseEntity.ok( - customerRepository.findAllWithPets().stream() + customers.stream() .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) .collect(Collectors.toList()) ); @@ -194,7 +201,7 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index c6ff1c60..9519b8c5 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -173,7 +173,7 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 3b3d121d..867a8c98 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -99,6 +99,8 @@ public class AppointmentService { public AppointmentResponse createAppointment(AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Customer customer = customerRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); @@ -108,7 +110,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -120,7 +122,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); @@ -142,6 +144,8 @@ public class AppointmentService { public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) { validateAppointmentRequest(request); + User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); + Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); @@ -154,7 +158,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); @@ -166,7 +170,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); @@ -251,12 +255,12 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId) { + private Set fetchCustomerPets(List customerPetIds, Long customerId, User.Role authenticatedRole) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { + if (authenticatedRole != User.Role.ADMIN && !customerPet.getCustomer().getCustomerId().equals(customerId)) { throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); } customerPets.add(customerPet); @@ -333,7 +337,7 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) + .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } @@ -368,8 +372,7 @@ public class AppointmentService { return true; } - private void validateStoreAccess(Long requestedStoreId) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; } diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index b7d97e74..e0adc2b6 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -15,7 +15,12 @@ import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.SupplierRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import java.util.List; import java.util.Optional; @@ -26,20 +31,32 @@ import static org.mockito.Mockito.when; class DropdownControllerTest { - @Test - void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); + private PetRepository petRepository; + private CustomerRepository customerRepository; + private CustomerPetRepository customerPetRepository; + private ServiceRepository serviceRepository; + private ProductRepository productRepository; + private CategoryRepository categoryRepository; + private StoreRepository storeRepository; + private SupplierRepository supplierRepository; + private EmployeeStoreRepository employeeStoreRepository; + private UserRepository userRepository; + private DropdownController controller; - DropdownController controller = new DropdownController( + @BeforeEach + void setUp() { + petRepository = mock(PetRepository.class); + customerRepository = mock(CustomerRepository.class); + customerPetRepository = mock(CustomerPetRepository.class); + serviceRepository = mock(ServiceRepository.class); + productRepository = mock(ProductRepository.class); + categoryRepository = mock(CategoryRepository.class); + storeRepository = mock(StoreRepository.class); + supplierRepository = mock(SupplierRepository.class); + employeeStoreRepository = mock(EmployeeStoreRepository.class); + userRepository = mock(UserRepository.class); + + controller = new DropdownController( petRepository, customerRepository, customerPetRepository, @@ -51,7 +68,25 @@ class DropdownControllerTest { employeeStoreRepository, userRepository ); + } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + private void setAuthentication(Long userId, User.Role role) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken( + new AppPrincipal(userId, "user", role, 0), + null, + List.of() + ) + ); + } + + @Test + void getStoreEmployeesReturnsBothStaffAndAdminLinkedEmployees() { StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -86,37 +121,13 @@ class DropdownControllerTest { var response = controller.getStoreEmployees(1L); - assertEquals(1, response.getBody().size()); + assertEquals(2, response.getBody().size()); assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - assertEquals("Alex Jones", response.getBody().get(0).getLabel()); + assertEquals(Long.valueOf(8L), response.getBody().get(1).getId()); } @Test void getStoreEmployeesExcludesInactiveStaffUsers() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); - - DropdownController controller = new DropdownController( - petRepository, - customerRepository, - customerPetRepository, - serviceRepository, - productRepository, - categoryRepository, - storeRepository, - supplierRepository, - employeeStoreRepository, - userRepository - ); - StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -142,30 +153,33 @@ class DropdownControllerTest { } @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPets() { - PetRepository petRepository = mock(PetRepository.class); - CustomerRepository customerRepository = mock(CustomerRepository.class); - CustomerPetRepository customerPetRepository = mock(CustomerPetRepository.class); - ServiceRepository serviceRepository = mock(ServiceRepository.class); - ProductRepository productRepository = mock(ProductRepository.class); - CategoryRepository categoryRepository = mock(CategoryRepository.class); - StoreRepository storeRepository = mock(StoreRepository.class); - SupplierRepository supplierRepository = mock(SupplierRepository.class); - EmployeeStoreRepository employeeStoreRepository = mock(EmployeeStoreRepository.class); - UserRepository userRepository = mock(UserRepository.class); + void getAppointmentCustomersReturnsOnlyCustomersWithPetsForStaff() { + User staffUser = new User(); + staffUser.setId(99L); + staffUser.setRole(User.Role.STAFF); + when(userRepository.findById(99L)).thenReturn(Optional.of(staffUser)); + setAuthentication(99L, User.Role.STAFF); - DropdownController controller = new DropdownController( - petRepository, - customerRepository, - customerPetRepository, - serviceRepository, - productRepository, - categoryRepository, - storeRepository, - supplierRepository, - employeeStoreRepository, - userRepository - ); + Customer one = new Customer(); + one.setCustomerId(1L); + one.setFirstName("Alex"); + one.setLastName("Brown"); + + when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); + + var response = controller.getAppointmentCustomers(); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); + } + + @Test + void getAppointmentCustomersReturnsAllCustomersForAdmin() { + User adminUser = new User(); + adminUser.setId(88L); + adminUser.setRole(User.Role.ADMIN); + when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); + setAuthentication(88L, User.Role.ADMIN); Customer one = new Customer(); one.setCustomerId(1L); @@ -177,12 +191,12 @@ class DropdownControllerTest { two.setFirstName("Emily"); two.setLastName("Clark"); - when(customerRepository.findAllWithPets()).thenReturn(List.of(one, two)); + when(customerRepository.findAll()).thenReturn(List.of(one, two)); var response = controller.getAppointmentCustomers(); assertEquals(2, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - assertEquals("Alex Brown", response.getBody().get(0).getLabel()); + assertEquals(Long.valueOf(2L), response.getBody().get(1).getId()); } } diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 6192ccb2..0f3a47a1 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -87,10 +87,11 @@ class AdoptionServiceTest { } @Test - void createAdoptionAutoAssignsFirstStaffEmployee() { + void createAdoptionAutoAssignsFirstAssignableEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); + // resolveAdoptionEmployee uses the first one from the list returned by repo + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(staffEmployee, adminEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); adoption.setAdoptionId(10L); @@ -110,10 +111,15 @@ class AdoptionServiceTest { } @Test - void createAdoptionRejectsAdminEmployeeSelection() { + void createAdoptionAllowsAdminEmployeeSelection() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); + when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { + Adoption adoption = invocation.getArgument(0); + adoption.setAdoptionId(10L); + return adoption; + }); AdoptionRequest request = new AdoptionRequest(); request.setPetId(1L); @@ -122,7 +128,9 @@ class AdoptionServiceTest { request.setAdoptionDate(LocalDate.now()); request.setAdoptionStatus("Pending"); - assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); + var response = adoptionService.createAdoption(request); + + assertEquals(8L, response.getEmployeeId()); } @Test diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 9d9daa40..7146248c 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -18,6 +18,7 @@ import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.security.AppPrincipal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -41,39 +42,22 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class AppointmentServiceTest { - @Mock - private AppointmentRepository appointmentRepository; - - @Mock - private CustomerRepository customerRepository; - - @Mock - private CustomerPetRepository customerPetRepository; - - @Mock - private PetRepository petRepository; - - @Mock - private ServiceRepository serviceRepository; - - @Mock - private StoreRepository storeRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private EmployeeRepository employeeRepository; - - @Mock - private EmployeeStoreRepository employeeStoreRepository; + @Mock private AppointmentRepository appointmentRepository; + @Mock private CustomerRepository customerRepository; + @Mock private CustomerPetRepository customerPetRepository; + @Mock private ServiceRepository serviceRepository; + @Mock private PetRepository petRepository; + @Mock private StoreRepository storeRepository; + @Mock private UserRepository userRepository; + @Mock private EmployeeRepository employeeRepository; + @Mock private EmployeeStoreRepository employeeStoreRepository; @InjectMocks private AppointmentService appointmentService; @@ -89,6 +73,13 @@ class AppointmentServiceTest { @BeforeEach void setUp() { + setAuthentication(99L, User.Role.ADMIN); + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + customer = new Customer(); customer.setCustomerId(1L); customer.setFirstName("Pat"); @@ -131,7 +122,6 @@ class AppointmentServiceTest { when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); date = LocalDate.now().plusDays(1); - } @AfterEach @@ -164,62 +154,10 @@ class AppointmentServiceTest { } @Test - void cancelledAppointmentsDoNotBlockAvailability() { - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - - List slots = appointmentService.checkAvailability(1L, 1L, date); - - assertTrue(slots.contains("10:00")); - } - - @Test - void updateAppointmentDoesNotConflictWithItself() { - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - User user = new User(); - user.setId(10L); - user.setUsername("pat"); - user.setRole(User.Role.CUSTOMER); - user.setTokenVersion(0); - when(userRepository.findById(10L)).thenReturn(Optional.of(user)); - setAuthentication(10L, User.Role.CUSTOMER); - - when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setPetIds(List.of(1L)); - - var response = appointmentService.updateAppointment(1L, request); - - assertEquals(1L, response.getAppointmentId()); - assertEquals("Booked", response.getAppointmentStatus()); - } - - @Test - void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Customer otherCustomer = new Customer(); otherCustomer.setCustomerId(2L); @@ -250,26 +188,63 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsCustomerOwnedCustomerPet() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); + void createAppointmentAllowsCustomerPetOwnedByDifferentCustomerForAdmin() { setAuthentication(99L, User.Role.ADMIN); + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(2L); + + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + otherCustomerPet.setPetName("Not Yours"); + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appt = invocation.getArgument(0); + appt.setAppointmentId(101L); + return appt; + }); + + var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); + request.setCustomerId(1L); + request.setStoreId(1L); + request.setServiceId(1L); + request.setAppointmentDate(date); + request.setAppointmentTime(LocalTime.of(10, 0)); + request.setAppointmentStatus("Booked"); + request.setCustomerPetIds(List.of(22L)); + + var response = appointmentService.createAppointment(request); + assertEquals(101L, response.getAppointmentId()); + } + + @Test + void createAppointmentAllowsAnyPetForAdmin() { + setAuthentication(99L, User.Role.ADMIN); + + Customer otherCustomer = new Customer(); + otherCustomer.setCustomerId(22L); + CustomerPet otherCustomerPet = new CustomerPet(); + otherCustomerPet.setCustomerPetId(22L); + otherCustomerPet.setCustomer(otherCustomer); + + when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); + when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store))); + when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(99L); + appointment.setAppointmentId(101L); return appointment; }); @@ -280,24 +255,19 @@ class AppointmentServiceTest { request.setAppointmentDate(date); request.setAppointmentTime(LocalTime.of(10, 0)); request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(11L)); + request.setCustomerPetIds(List.of(22L)); var response = appointmentService.createAppointment(request); - assertEquals(99L, response.getAppointmentId()); + assertEquals(101L, response.getAppointmentId()); assertEquals(1L, response.getCustomerId()); - assertEquals(7L, response.getEmployeeId()); } @Test - void createAppointmentRejectsAdminEmployeeSelection() { - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setUsername("admin"); - adminUser.setRole(User.Role.ADMIN); - adminUser.setTokenVersion(0); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); + void createAppointmentAllowsAdminEmployeeSelection() { + setAuthentication(7L, User.Role.STAFF); + when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); + when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); Employee adminEmployee = new Employee(); adminEmployee.setEmployeeId(8L); @@ -320,6 +290,11 @@ class AppointmentServiceTest { when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); + when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { + Appointment appointment = invocation.getArgument(0); + appointment.setAppointmentId(102L); + return appointment; + }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -331,19 +306,20 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(11L)); - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); + var response = appointmentService.createAppointment(request); + + assertEquals(102L, response.getAppointmentId()); + assertEquals(8L, response.getEmployeeId()); } @Test void createAppointmentRejectsInactiveStaffUserSelection() { + setAuthentication(99L, User.Role.ADMIN); User adminUser = new User(); adminUser.setId(99L); - adminUser.setUsername("admin"); adminUser.setRole(User.Role.ADMIN); adminUser.setActive(true); - adminUser.setTokenVersion(0); when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - setAuthentication(99L, User.Role.ADMIN); User inactiveStaffUser = new User(); inactiveStaffUser.setId(7L); @@ -384,13 +360,14 @@ class AppointmentServiceTest { appointment.setEmployee(employee); appointment.setCustomer(customer); appointment.setPets(Set.of()); + appointment.setCustomerPets(Set.of()); return appointment; } private void setAuthentication(Long userId, User.Role role) { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken( - new com.petshop.backend.security.AppPrincipal(userId, "user", role, 0), + new AppPrincipal(userId, "user", role, 0), "n/a", List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) ) From 153ec836cf1efa40a8c1be354aae6aacbc28bcd7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:03:29 -0600 Subject: [PATCH 057/137] Restrict assignments to staff --- .../controller/DropdownController.java | 2 +- .../backend/service/AdoptionService.java | 2 +- .../backend/service/AppointmentService.java | 2 +- .../controller/DropdownControllerTest.java | 5 ++--- .../backend/service/AdoptionServiceTest.java | 17 +++++---------- .../service/AppointmentServiceTest.java | 21 +++++++------------ 6 files changed, 18 insertions(+), 31 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index b307174c..2217b4e7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -201,7 +201,7 @@ public class DropdownController { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .filter(user -> user.getRole() == User.Role.STAFF) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 9519b8c5..c6ff1c60 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -173,7 +173,7 @@ public class AdoptionService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .filter(user -> user.getRole() == User.Role.STAFF) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 867a8c98..363cb3a0 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -337,7 +337,7 @@ public class AppointmentService { return false; } return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF || user.getRole() == User.Role.ADMIN) + .filter(user -> user.getRole() == User.Role.STAFF) .filter(user -> Boolean.TRUE.equals(user.getActive())) .isPresent(); } diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index e0adc2b6..563e5d2f 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -86,7 +86,7 @@ class DropdownControllerTest { } @Test - void getStoreEmployeesReturnsBothStaffAndAdminLinkedEmployees() { + void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { StoreLocation store = new StoreLocation(); store.setStoreId(1L); @@ -121,9 +121,8 @@ class DropdownControllerTest { var response = controller.getStoreEmployees(1L); - assertEquals(2, response.getBody().size()); + assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - assertEquals(Long.valueOf(8L), response.getBody().get(1).getId()); } @Test diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 0f3a47a1..1fef0e42 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -87,11 +87,11 @@ class AdoptionServiceTest { } @Test - void createAdoptionAutoAssignsFirstAssignableEmployee() { + void createAdoptionAutoAssignsFirstStaffEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - // resolveAdoptionEmployee uses the first one from the list returned by repo - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(staffEmployee, adminEmployee)); + // resolveAdoptionEmployee filters for staff + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); adoption.setAdoptionId(10L); @@ -111,15 +111,10 @@ class AdoptionServiceTest { } @Test - void createAdoptionAllowsAdminEmployeeSelection() { + void createAdoptionRejectsAdminEmployeeSelection() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { - Adoption adoption = invocation.getArgument(0); - adoption.setAdoptionId(10L); - return adoption; - }); AdoptionRequest request = new AdoptionRequest(); request.setPetId(1L); @@ -128,9 +123,7 @@ class AdoptionServiceTest { request.setAdoptionDate(LocalDate.now()); request.setAdoptionStatus("Pending"); - var response = adoptionService.createAdoption(request); - - assertEquals(8L, response.getEmployeeId()); + assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); } @Test diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index 7146248c..d4892126 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -264,10 +264,13 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsAdminEmployeeSelection() { - setAuthentication(7L, User.Role.STAFF); - when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); - when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); + void createAppointmentRejectsAdminEmployeeSelection() { + setAuthentication(99L, User.Role.ADMIN); + User adminUser = new User(); + adminUser.setId(99L); + adminUser.setRole(User.Role.ADMIN); + adminUser.setActive(true); + when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); Employee adminEmployee = new Employee(); adminEmployee.setEmployeeId(8L); @@ -290,11 +293,6 @@ class AppointmentServiceTest { when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(102L); - return appointment; - }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -306,10 +304,7 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(11L)); - var response = appointmentService.createAppointment(request); - - assertEquals(102L, response.getAppointmentId()); - assertEquals(8L, response.getEmployeeId()); + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } @Test From 521537dc8f8642e97fe2e92544dff61f2b4b3600 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 16:17:58 -0600 Subject: [PATCH 058/137] Enforce staff-only assignments --- .../resources/db/migration/V16__activate_all_employees.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V16__activate_all_employees.sql diff --git a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql new file mode 100644 index 00000000..cbabc11d --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql @@ -0,0 +1,5 @@ +-- Activate all employees in the users table so they appear in dropdowns +UPDATE users u +SET u.active = TRUE +WHERE u.role IN ('STAFF', 'ADMIN') + AND EXISTS (SELECT 1 FROM employee e WHERE e.user_id = u.id); From 768103cbac310a8032072cdab5a75bddda68579d Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:16:40 -0600 Subject: [PATCH 059/137] Added Helper class and commented most fragments --- .../activities/HomeActivity.java | 20 +- .../activities/MainActivity.java | 9 + .../petstoremobile/adapters/PetAdapter.java | 24 +- .../adapters/ProductAdapter.java | 22 +- .../fragments/ChatFragment.java | 95 +++++-- .../fragments/ListFragment.java | 13 +- .../fragments/ProfileFragment.java | 258 ++++-------------- .../listfragments/AdoptionFragment.java | 37 ++- .../listfragments/AppointmentFragment.java | 60 +++- .../listfragments/InventoryFragment.java | 58 +++- .../fragments/listfragments/PetFragment.java | 43 ++- .../listfragments/ProductFragment.java | 28 +- .../ProductSupplierFragment.java | 28 +- .../listfragments/PurchaseOrderFragment.java | 28 +- .../listfragments/ServiceFragment.java | 32 ++- .../listfragments/SupplierFragment.java | 32 ++- .../AdoptionDetailFragment.java | 57 +++- .../AppointmentDetailFragment.java | 79 +++++- .../InventoryDetailFragment.java | 51 +++- .../detailfragments/PetDetailFragment.java | 45 +-- .../ProductDetailFragment.java | 216 ++++++--------- .../ProductSupplierDetailFragment.java | 47 +++- .../PurchaseOrderDetailFragment.java | 7 +- .../ServiceDetailFragment.java | 40 ++- .../SupplierDetailFragment.java | 40 ++- .../PetProfileFragment.java | 191 +++---------- .../petstoremobile/utils/ErrorUtils.java | 32 +++ .../petstoremobile/utils/GlideUtils.java | 122 +++++++++ .../utils/ImagePickerHelper.java | 160 +++++++++++ .../example/petstoremobile/utils/UIUtils.java | 18 ++ 30 files changed, 1226 insertions(+), 666 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 09bcea9d..947b98e1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -38,6 +38,9 @@ public class HomeActivity extends AppCompatActivity { } }); + /** + * Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents. + */ @Override protected void onCreate(Bundle savedInstanceState) { EdgeToEdge.enable(this); @@ -71,12 +74,18 @@ public class HomeActivity extends AppCompatActivity { requestNotificationPermission(); } + /** + * Handles new intents received while the activity is already running (like notifications). + */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleIntent(intent); } + /** + * Processes the intent to determine if specific navigation (like opening a chat) is required. + */ private void handleIntent(Intent intent) { if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { Bundle args = new Bundle(); @@ -90,14 +99,17 @@ public class HomeActivity extends AppCompatActivity { } } - // Function to start the notification service in the background - // to receive notifications when a new conversation is created + /** + * Starts the background service responsible for monitoring chat notifications. + */ private void startNotificationService() { Intent serviceIntent = new Intent(this, ChatNotificationService.class); startService(serviceIntent); } - //Function to request for notification permission + /** + * Requests POST_NOTIFICATIONS permission from the user if running on Android 13 and above. + */ private void requestNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { @@ -105,4 +117,4 @@ public class HomeActivity extends AppCompatActivity { } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index d090a09e..3d27aab3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -39,6 +39,9 @@ public class MainActivity extends AppCompatActivity { @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; + /** + * Initializes the activity, sets up the UI, and checks for an existing login session. + */ @Override protected void onCreate(Bundle savedInstanceState) { EdgeToEdge.enable(this); @@ -98,6 +101,9 @@ public class MainActivity extends AppCompatActivity { }); } + /** + * Executes the login process using the AuthViewModel and handles the authentication response. + */ private void performLogin(String username, String password) { viewModel.login(username, password).observe(this, resource -> { if (resource == null) return; @@ -129,6 +135,9 @@ public class MainActivity extends AppCompatActivity { }); } + /** + * Retrieves the logged-in user's profile information to save their ID before navigating to the home screen. + */ private void fetchUserIdAndNavigate() { viewModel.getMe().observe(this, resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index bec941f0..35c323eb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -9,13 +9,10 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.utils.GlideUtils; import java.util.List; public class PetAdapter extends RecyclerView.Adapter { @@ -93,25 +90,10 @@ public class PetAdapter extends RecyclerView.Adapter { holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); } - // Load pet image using Glide with circle crop + // Load pet image using Glide if (baseUrl != null) { String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); - - Object loadTarget = imageUrl; - if (token != null) { - loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() - .addHeader("Authorization", "Bearer " + token) - .build()); - } - - Glide.with(holder.itemView.getContext()) - .load(loadTarget) - .circleCrop() - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .placeholder(R.drawable.placeholder) - .error(R.drawable.placeholder) - .into(holder.ivPetProfile); + GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder); } else { holder.ivPetProfile.setImageResource(R.drawable.placeholder); } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 4e7c5fea..ad1cf678 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -6,13 +6,10 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.utils.GlideUtils; import java.util.List; public class ProductAdapter extends RecyclerView.Adapter { @@ -72,22 +69,7 @@ public class ProductAdapter extends RecyclerView.Adapter attachmentLauncher; - + /** + * Initializes the attachment launcher to handle file selection from the gallery. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -105,6 +107,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis ); } + /** + * Inflates the layout, initializes UI components, and sets up click listeners for messaging. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -155,7 +160,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return view; } - // Helper function to setup recycler views for chat and messages + /** + * Configures the RecyclerViews for the conversation list and the message history. + */ private void setupRecyclerViews() { // Set up Drawer menu to select conversation chatAdapter = new ChatAdapter(chatList, this); @@ -171,7 +178,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis setConversationActive(false); } - //Helper function to load token and user id then connect to websocket + /** + * Loads authentication tokens and user info, then initializes the Stomp WebSocket connection. + */ private void loadInitialData() { String token = tokenManager.getToken(); currentUserId = tokenManager.getUserId(); @@ -198,7 +207,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis loadCustomers(); } - //Helper function to load customer names for it to be displayed on drawer menu + /** + * Fetches a list of customers from the API to display customer names for the chat list. + */ private void loadCustomers() { customerApi.getAllCustomers(0, 100).enqueue(new Callback>() { @Override @@ -220,7 +231,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } - //helper function to load conversations entities to display with customer names in drawer menu + /** + * Retrieves all conversations for the current user and populates the chat drawer. + */ private void loadConversations() { chatApi.getAllConversations().enqueue(new Callback>() { @Override @@ -268,8 +281,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } - // Called when user taps a chat in the drawer - // Loads messages for that chat selected + /** + * Handles selection of a chat from the drawer, updating the UI and subscribing to the WebSocket. + */ @Override public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); @@ -284,7 +298,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis loadMessageHistory(activeConversationId); } - //helper function to load messages for selected chat + /** + * Fetches the full message history for a specific conversation from the API. + */ private void loadMessageHistory(Long conversationId) { messageApi.getMessages(conversationId).enqueue(new Callback>() { @Override @@ -307,7 +323,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } - //Helper function to send a message to the chat + /** + * Sends a plain text message to the currently active conversation. + */ private void sendMessage() { //check if a chat is selected if (activeConversationId == null) return; @@ -340,14 +358,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); } - //Helper function to open file picker when the attachment button is clicked + /** + * Launches a file picker intent to select an attachment for the message. + */ private void selectAttachment() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); attachmentLauncher.launch(intent); } - //Helper function to show the attachment preview + /** + * Displays a preview of the selected attachment in the UI. + */ private void showAttachmentPreview(Uri uri) { pendingAttachmentUri = uri; layoutAttachmentPreview.setVisibility(View.VISIBLE); @@ -365,13 +387,17 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - //Helper function to remove the attachment + /** + * Clears the current attachment selection and hides the preview UI. + */ private void removeAttachment() { pendingAttachmentUri = null; layoutAttachmentPreview.setVisibility(View.GONE); } - //Helper function to get the file name from the uri to display in attachment preview + /** + * Show the display name of the file from its Uri. + */ private String getFileName(Uri uri) { String result = null; if (uri.getScheme().equals("content")) { @@ -394,7 +420,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return result; } - //Helper function to send the message with attachment + /** + * Handles sending a message that includes a file attachment. + */ private void sendWithAttachment(Uri uri) { if (activeConversationId == null) return; @@ -402,7 +430,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis Log.d(TAG, "Send with attachment happening"); } - // When a message is received updates the chat preview + /** + * Callback triggered when a new message is received via the WebSocket. + */ @Override public void onMessageReceived(MessageDTO dto) { //if there is no active selected conversation or the message received is for another chat, then just update the preview of last message @@ -420,7 +450,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis scrollToBottom(); } - // When a new conversation is added, updates the chat preview + /** + * Callback triggered when a conversation is created or updated via the WebSocket. + */ @Override public void onConversationUpdated(ConversationDTO dto) { boolean updated = false; @@ -460,6 +492,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Callback triggered when the WebSocket connection is successfully opened. + */ @Override public void onSocketOpened() { if (!isAdded()) { @@ -471,6 +506,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } + /** + * Callback triggered when the WebSocket connection is closed. + */ @Override public void onSocketClosed() { if (!isAdded()) { @@ -479,6 +517,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis loadConversations(); } + /** + * Callback triggered when a WebSocket connection error occurs. + */ @Override public void onSocketError() { if (!isAdded()) { @@ -490,7 +531,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - // Helper function to convert DTO to message + /** + * Converts a MessageDTO into a Message object. + */ private Message dtoToModel(MessageDTO dto) { Message m = new Message(); m.setId(dto.getId()); @@ -505,7 +548,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis return m; } - //Helper function to scroll to bottom of the chat + /** + * Scrolls the message history RecyclerView to the most recent message. + */ private void scrollToBottom() { if (!messageList.isEmpty()) { rvMessages.post(() -> @@ -513,7 +558,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - // Helper function to update the chat preview last message + /** + * Updates the preview snippet of the last message for a specific conversation in the drawer. + */ private void updateConversationPreview(Long conversationId, String lastMessage) { if (conversationId == null) { return; @@ -535,7 +582,9 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - //Helper function to enable or disable the send button when there is no active chat + /** + * Toggles the UI state based on whether a conversation is currently selected. + */ private void setConversationActive(boolean active) { btnSend.setEnabled(active); etMessage.setEnabled(active); @@ -558,11 +607,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } } - // When fragment is destroyed, disconnect from websocket + /** + * Disconnects the WebSocket manager when the fragment view is destroyed. + */ @Override public void onDestroyView() { super.onDestroyView(); ChatNotificationService.activeConversationIdInUi = null; if (stompChatManager != null) stompChatManager.disconnect(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index af3de053..5a2dcfc6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -38,6 +38,9 @@ public class ListFragment extends Fragment { @Inject TokenManager tokenManager; + /** + * Inflates the fragment layout, initializes navigation drawers, and applies role-based access control. + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -106,6 +109,9 @@ public class ListFragment extends Fragment { return view; } + /** + * Initializes the NavController for the internal fragment container. + */ @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -116,6 +122,9 @@ public class ListFragment extends Fragment { } } + /** + * Navigates to a specific inner destination and closes all drawers. + */ private void navigateTo(int destinationId) { if (innerNavController != null) { innerNavController.navigate(destinationId); @@ -123,7 +132,9 @@ public class ListFragment extends Fragment { drawerLayout.closeDrawers(); } - //helper function to open the drawer + /** + * Programmatically opens the navigation drawer. + */ public void openDrawer() { drawerLayout.openDrawer(GravityCompat.START); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index f470ab31..b4f15e69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -1,21 +1,11 @@ package com.example.petstoremobile.fragments; -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; -import android.provider.MediaStore; -import android.text.InputType; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -26,26 +16,21 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.activities.MainActivity; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.dtos.ErrorResponse; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.services.ChatNotificationService; +import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; -import com.google.gson.Gson; +import com.example.petstoremobile.utils.UIUtils; import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -59,13 +44,15 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Fragment that displays and allows editing of the user's profile information. + */ @AndroidEntryPoint public class ProfileFragment extends Fragment { //initialize the view/controls private ImageView imgProfile; private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole; - private Uri photoUri; private UserDTO currentUser; private boolean hasImage = false; @@ -73,71 +60,31 @@ public class ProfileFragment extends Fragment { @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; - //Initialize the launchers for camera and gallery - private ActivityResultLauncher galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher permissionLauncher; + private ImagePickerHelper imagePickerHelper; - //Called when the fragment is created, sets up the launchers is set profile image + /** + * Initializes activity launchers and the ImagePickerHelper for camera and gallary. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Launcher to open gallery to select profile image - galleryLauncher = registerForActivityResult( - //open gallery - new ActivityResultContracts.StartActivityForResult(), - result -> { - //if the user selects an image and its not null - if (result.getResultCode() == Activity.RESULT_OK - && result.getData() != null) { - //get the selected image and set the image to the profile - Uri selectedImage = result.getData().getData(); - uploadAvatar(selectedImage); - } - } - ); + imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() { + @Override + public void onImagePicked(Uri uri) { + uploadAvatar(uri); + } - // Launcher for camera to open and capture profile image - cameraLauncher = registerForActivityResult( - //open camera - new ActivityResultContracts.TakePicture(), - success -> { - //if a photo is taken set the image profile to it otherwise do nothing - if (success) { - uploadAvatar(photoUri); - } - } - ); - - // Launcher to request camera permission - permissionLauncher = registerForActivityResult( - //ask user for camera permission - new ActivityResultContracts.RequestPermission(), - granted -> { - //if the permission is granted launch the camera - if (granted) { - launchCamera(); - } - else { - //if the permission is denied then tell the user to grant it - new AlertDialog.Builder(requireContext()) - .setTitle("Permission Permission Required") - .setMessage("Please grant camera permission to use this feature") - .setPositiveButton("Open Settings", (dialog, which) ->{ - //open the settings page to grant the permission when they click open settings - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null)); - startActivity(intent); - }) - //close the dialog when the user clicks cancel - .setNegativeButton("Cancel", null) - .show(); - } - } - ); + @Override + public void onImageRemoved() { + deleteAvatar(); + } + }); } + /** + * Inflates the fragment layout and sets up listeners for profile. + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -160,39 +107,7 @@ public class ProfileFragment extends Fragment { //Set up listeners for the buttons //Change photo button btnChangePhoto.setOnClickListener(v -> { - List options = new ArrayList<>(); - options.add("Take Photo"); - options.add("Choose from Gallery"); - if (hasImage) { - options.add("Remove Photo"); - } - - //Show alert dialog to user to select from gallery or camera - new AlertDialog.Builder(requireContext()) - .setTitle("Change Profile Photo") - //set the options for the alert dialog - .setItems(options.toArray(new String[0]), (dialog, which) -> { - String selected = options.get(which); - if (selected.equals("Take Photo")) { - // Choose Camera - //Checks if the user has granted the camera permission already - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - //if the permission is already granted then launch the camera - launchCamera(); - } else { - //otherwise request the permission - permissionLauncher.launch(Manifest.permission.CAMERA); - } - } else if (selected.equals("Choose from Gallery")) { - // Choose Gallery - Intent intent = new Intent(Intent.ACTION_PICK, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - galleryLauncher.launch(intent); - } else if (selected.equals("Remove Photo")) { - deleteAvatar(); - } - }) - .show(); + imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage); }); //Edit email button @@ -231,11 +146,10 @@ public class ProfileFragment extends Fragment { input.setText(tvProfilePhone.getText().toString()); //set input type to phone number - input.setInputType(InputType.TYPE_CLASS_PHONE); + input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE); - //add canada phone number formatting to input (XXX) XXX-XXXX - input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA")); - input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)}); + //add canada phone number formatting to input + UIUtils.formatPhoneInput(input); //Show alert dialog to user to enter new phone @@ -256,13 +170,13 @@ public class ProfileFragment extends Fragment { //Logout button btnLogout.setOnClickListener(v -> { // Stop notification service before logging out so notifications stop - Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class); + android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class); requireContext().stopService(serviceIntent); tokenManager.clearLoginData(); // clear the token for next login //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen - Intent intent = new Intent(getActivity(), MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class); + intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK); //start the activity to go to login page and finish the current activity startActivity(intent); requireActivity().finish(); @@ -271,17 +185,9 @@ public class ProfileFragment extends Fragment { return view; } - //Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo - private void launchCamera() { - //create a file in the cache directory to store the photo in - File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg"); - //get the uri for the file made - photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); - //launch the camera to capture the photo and save the photo to photoUri - cameraLauncher.launch(photoUri); - } - - //Helper function to call the backend to get profile data and load it to the view + /** + * Fetches current user profile data from the API and then updates the UI. + */ private void loadProfileData() { authApi.getMe().enqueue(new Callback() { @Override @@ -300,44 +206,21 @@ public class ProfileFragment extends Fragment { String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; String token = tokenManager.getToken(); - if (token != null) { - // Create GlideUrl with token to fetch the image - GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder() - .addHeader("Authorization", "Bearer " + token) - .build()); + GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } - // Load image using Glide - Glide.with(ProfileFragment.this) - .load(glideUrl) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .placeholder(R.drawable.placeholder) - .error(R.drawable.placeholder) - .listener(new com.bumptech.glide.request.RequestListener() { - @Override - public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { - hasImage = false; - return false; - } - - @Override - public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { - hasImage = true; - return false; - } - }) - .into(imgProfile); - } else { - // load placeholder image if token is null - hasImage = false; - Glide.with(ProfileFragment.this) - .load(R.drawable.placeholder) - .into(imgProfile); - } + @Override + public void onLoadFailed() { + hasImage = false; + } + }); } else { Log.e("onResponse: ", response.message()); - Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to load profile"); } } @@ -349,10 +232,12 @@ public class ProfileFragment extends Fragment { }); } - //Helper function to call the backend to upload a profile image + /** + * Uploads the selected or captured image as the user's new avatar. + */ private void uploadAvatar(Uri uri) { try { - File file = getFileFromUri(uri); + File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) return; // Create RequestBody for file upload @@ -369,7 +254,7 @@ public class ProfileFragment extends Fragment { // Reload image after successful upload loadProfileData(); } else { - Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to upload avatar"); } } @@ -384,6 +269,9 @@ public class ProfileFragment extends Fragment { } } + /** + * Sends a request to the API to delete the current user's avatar image. + */ private void deleteAvatar() { authApi.deleteAvatar().enqueue(new Callback() { @Override @@ -393,7 +281,7 @@ public class ProfileFragment extends Fragment { hasImage = false; imgProfile.setImageResource(R.drawable.placeholder); } else { - Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to remove avatar"); } } @@ -405,27 +293,9 @@ public class ProfileFragment extends Fragment { }); } - // Helper function to create a temporary File object from a Uri for uploading the avatar - private File getFileFromUri(Uri uri) { - try { - InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); - File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg"); - FileOutputStream outputStream = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, length); - } - outputStream.close(); - inputStream.close(); - return tempFile; - } catch (Exception e) { - Log.e("FILE_UTILS", "Error creating temp file", e); - return null; - } - } - - //Helper function to update a profile field in the backend + /** + * Updates a specific profile field (like email or phone) by sending a request to the API. + */ private void updateProfileField(String fieldName, String value) { Map updates = new HashMap<>(); updates.put(fieldName, value); @@ -440,15 +310,7 @@ public class ProfileFragment extends Fragment { tvProfilePhone.setText(currentUser.getPhone()); Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); } else { - try { - String errorJson = response.errorBody().string(); - ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class); - String errorMessage = errorResponse.getMessage(); - Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e("UPDATE_PROFILE", "Error parsing error body", e); - Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show(); - } + ErrorUtils.showErrorMessage(getContext(), response, "Failed to update profile"); } } @@ -459,4 +321,4 @@ public class ProfileFragment extends Fragment { } }); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 69bcee15..cae5fd2f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -48,12 +48,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); } + /** + * Sets up the fragment's UI components, including RecyclerView, Search, SwipeRefresh, and Calendar. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -87,6 +93,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop return view; } + /** + * Toggles the calendar display between week and month modes. + */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; calendarView.state().edit() @@ -94,6 +103,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop .commit(); } + /** + * Sets up the date selection listener for the calendar. + */ private void setupCalendar() { calendarView.setOnDateChangedListener(new OnDateSelectedListener() { @Override @@ -113,6 +125,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Updates the calendar decorators to highlight days with adoptions. + */ private void updateCalendarDecorators() { HashSet datesWithAdoptions = new HashSet<>(); for (AdoptionDTO adoption : adoptionList) { @@ -133,6 +148,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } + /** + * Initializes the RecyclerView for displaying adoptions. + */ private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); adapter = new AdoptionAdapter(filteredList, this); @@ -140,6 +158,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop rv.setAdapter(adapter); } + /** + * Sets up the search bar for filtering + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchAdoption); etSearch.addTextChangedListener(new TextWatcher() { @@ -151,11 +172,17 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Sets up the SwipeRefreshLayout to reload adoption data. + */ private void setupSwipeRefresh(View view) { swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption); swipeRefresh.setOnRefreshListener(this::loadAdoptions); } + /** + * Filters the adoption list based on search query and selected calendar date. + */ private void filter(String query) { filteredList.clear(); String lowerQuery = query.toLowerCase(); @@ -182,7 +209,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop adapter.notifyDataSetChanged(); } - // Helper function to get a list of all adoptions from the backend + /** + * Fetches the adoption list from the server through the ViewModel. + */ private void loadAdoptions() { //Load all adoptions from the backend using viewModel viewModel.getAllAdoptions(0, 500).observe(getViewLifecycleOwner(), resource -> { @@ -214,6 +243,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop }); } + /** + * Navigates to the adoption detail screen for a specific adoption or to create a new one. + */ private void openDetail(int position) { Bundle args = new Bundle(); @@ -229,6 +261,9 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); } + /** + * Handles item click in the adoption list. + */ @Override public void onAdoptionClick(int position) { openDetail(position); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index b284a2bf..f62475c0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -71,6 +71,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + /** + * Initializes the fragment and its associated ViewModels. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -79,6 +82,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); } + /** + * Sets up the fragment's UI, including RecyclerView, search, swipe-to-refresh, and calendar. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -114,7 +120,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return view; } - // Toggle Calendar Mode from week to month and other way around + /** + * Toggles the calendar between week and month display modes. + */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; calendarView.state().edit() @@ -122,6 +130,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } + /** + * Sets up the date selection listener for the calendar. + */ private void setupCalendar() { calendarView.setOnDateChangedListener(new OnDateSelectedListener() { @Override @@ -141,7 +152,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - //Set indicators for dates with appointments on the calendar + /** + * Updates calendar indicators to highlight dates that have scheduled appointments. + */ private void updateCalendarDecorators() { HashSet datesWithAppointments = new HashSet<>(); for (AppointmentDTO appointment : appointmentList) { @@ -163,6 +176,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchAppointment); etSearch.addTextChangedListener(new TextWatcher() { @@ -181,6 +197,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Filters the appointment list based on the search query and selected calendar date. + */ private void filterAppointments(String query) { filteredList.clear(); String lowerQuery = query.toLowerCase(); @@ -207,11 +226,17 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. adapter.notifyDataSetChanged(); } + /** + * Initializes the SwipeRefreshLayout to allow manual data refreshing. + */ private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment); swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData); } + /** + * Navigates to the appointment detail screen for editing or creating an appointment. + */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); @@ -230,20 +255,32 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } + + /** + * Reloads data when an appointment is saved. + */ public void onAppointmentSaved(int position, AppointmentDTO appointment) { loadAppointmentData(); } + /** + * Reloads data when an appointment is deleted. + */ public void onAppointmentDeleted(int position) { loadAppointmentData(); } + /** + * Handles item click in the appointment list. + */ @Override public void onAppointmentClick(int position) { openAppointmentDetails(position); } - // Helper function to get a list of all appointments from the backend + /** + * Fetches all appointment data from the server. + */ private void loadAppointmentData() { //Load all appointments from the backend using viewModel appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> { @@ -275,7 +312,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - // Load Pets + /** + * Fetches the full list of pets from the server. + */ private void loadPets() { petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -285,7 +324,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - // Load Services + /** + * Fetches the full list of services from the server. + */ private void loadServices() { serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -295,6 +336,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } + /** + * Get a pet's name based on its ID. + */ private String getPetName(Long id) { for (PetDTO p : petList) { if (p.getPetId().equals(id)) return p.getPetName(); @@ -303,6 +347,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return ""; } + /** + * Get a service's name based on its ID. + */ private String getServiceName(Long id) { for (ServiceDTO s : serviceList) { if (s.getServiceId().equals(id))return s.getServiceName(); @@ -310,6 +357,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. return ""; } + /** + * Initializes the RecyclerView for displaying appointments. + */ private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments); adapter = new AppointmentAdapter(filteredList, this); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index e77ae958..799fe39f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -74,12 +74,18 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn // Prevent spinner from firing on initial load private boolean spinnerReady = false; + /** + * Initializes the fragment and its ViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(InventoryViewModel.class); } + /** + * Sets up the fragment's UI components, including the inventory list, search, and category filter. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -114,7 +120,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return view; } - // Categories + /** + * Fetches all product categories to populate the filter spinner. + */ private void loadCategories() { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { @@ -128,6 +136,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } + /** + * Setup the category filter spinner. + */ private void setupCategorySpinner() { // First item is always "All Categories" List categoryNames = new ArrayList<>(); @@ -167,8 +178,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } - // Search - + /** + * Sets up the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchInventory); etSearch.addTextChangedListener(new TextWatcher() { @@ -193,7 +205,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - // RecyclerView + infinite scroll + /** + * Initializes the RecyclerView with a layout manager, and adapter. + */ private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewInventory); adapter = new InventoryAdapter(inventoryList, this); @@ -218,12 +232,17 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } + /** + * Sets up the SwipeRefreshLayout to reload the first page of inventory items. + */ private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); } - // Helper function to get a list of all inventory items from the backend + /** + * Fetches a page of inventory items from the API. + */ private void loadInventory(boolean reset) { if (isLoading) return; @@ -270,7 +289,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - // Combines search text and category into one query string for ?q= + /** + * Constructs a query string based on the current search text and selected category. + */ private String buildQuery() { String q = null; if (!currentQuery.isEmpty() && selectedCategory != null) { @@ -284,7 +305,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn return q; } - // Bulk delete + /** + * Displays a confirmation dialog before performing a bulk deletion of selected items. + */ private void confirmBulkDelete() { List ids = adapter.getSelectedIds(); if (ids.isEmpty()) @@ -298,6 +321,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn .show(); } + /** + * Executes the bulk deletion of inventory items through the ViewModel. + */ private void bulkDelete(List ids) { viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status != Resource.Status.LOADING) { @@ -313,6 +339,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } + /** + * Hides the bulk deletion UI bar. + */ private void hideBulkDeleteBar() { if (btnBulkDelete != null) btnBulkDelete.setVisibility(View.GONE); @@ -320,7 +349,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn tvSelectionCount.setVisibility(View.GONE); } - // Navigation + /** + * Navigates to the inventory detail screen for a specific item or to add a new one. + */ private void openDetail(InventoryDTO inv) { Bundle args = new Bundle(); @@ -335,12 +366,16 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); } + /** + * Reloads inventory data when changes occur. + */ public void onInventoryChanged() { loadInventory(true); } - // Adapter callbacks - + /** + * Handles item click in the inventory list. + */ @Override public void onInventoryClick(int position) { if (position >= 0 && position < inventoryList.size()) { @@ -348,6 +383,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } } + /** + * Updates the bulk deletion UI visibility and count when items are selected or deselected. + */ @Override public void onSelectionChanged(int selectedCount) { if (selectedCount > 0) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index fdbc38f4..44198344 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -29,7 +28,6 @@ import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.utils.Resource; import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; @@ -53,12 +51,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private EditText etSearch; private Spinner spinnerStatus; + /** + * Initializes the fragment and its associated PetViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PetViewModel.class); } + /** + * Sets up the fragment's UI components, including RecyclerView, search, status filter, and swipe-to-refresh. + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -72,7 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupSwipeRefresh(view); FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet); - fabAddPet.setOnClickListener(v -> openPetDetails(-1)); + fabAddPet.setOnClickListener(v -> openPetDetails()); hamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); @@ -87,12 +91,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return view; } + /** + * Reloads pet data every time the fragment becomes visible. + */ @Override public void onResume() { super.onResume(); loadPetData(); } + /** + * Configures the search bar with a for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchPet); etSearch.addTextChangedListener(new TextWatcher() { @@ -104,6 +114,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } + /** + * Configures the status filter spinner. + */ private void setupStatusFilter(View view) { spinnerStatus = view.findViewById(R.id.spinnerStatus); String[] statuses = {"All Statuses", "Available", "Adopted"}; @@ -122,6 +135,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } + /** + * Filters the pet list based on both the search query and the selected status. + */ private void filterPets() { String query = etSearch.getText().toString().toLowerCase(); String selectedStatus = spinnerStatus.getSelectedItem().toString(); @@ -143,11 +159,17 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen adapter.notifyDataSetChanged(); } + /** + * Sets up the SwipeRefreshLayout to allow manual re-fetching of pet data. + */ private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet); swipeRefreshLayout.setOnRefreshListener(this::loadPetData); } + /** + * Navigates to the pet profile screen for a specific pet. + */ private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = filteredList.get(position); @@ -167,16 +189,24 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } - private void openPetDetails(int position) { + /** + * Navigates to the pet detail screen. (Only used for adding a new pet on this screen) + */ + private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } + /** + * Handles clicks on individual pet items in the list. + */ @Override public void onPetClick(int position) { openPetProfile(position); } - // Helper function to get a list of all pets from the backend + /** + * Fetches all pet data from the server via the ViewModel and updates the UI. + */ private void loadPetData() { //Load all pets from the backend using viewModel viewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -207,6 +237,9 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } + /** + * Initializes the RecyclerView with a layout manager and adapter for displaying pets. + */ private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); adapter = new PetAdapter(filteredList, this); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index d56da91a..61adfcf5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -41,12 +41,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + /** + * Initializes the fragment and its associated ProductViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductViewModel.class); } + /** + * Sets up the fragment's UI components, including the product list, search, and swipe-to-refresh. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -75,6 +81,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc return view; } + /** + * Initializes the RecyclerView with a layout manager and adapter for displaying products. + */ private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewProducts); adapter = new ProductAdapter(filteredList, this); @@ -84,6 +93,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc rv.setAdapter(adapter); } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchProduct); etSearch.addTextChangedListener(new TextWatcher() { @@ -95,11 +107,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc }); } + /** + * Sets up the SwipeRefreshLayout to allow manual re-fetching of product data. + */ private void setupSwipeRefresh(View view) { swipeRefresh = view.findViewById(R.id.swipeRefreshProduct); swipeRefresh.setOnRefreshListener(this::loadProducts); } + /** + * Filters the product list based on the search query across name, category, and description. + */ private void filter() { String query = etSearch.getText().toString().toLowerCase(); @@ -117,7 +135,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc adapter.notifyDataSetChanged(); } - // Helper function to get a list of all products from the backend + /** + * Fetches all product data from the server through the ViewModel and updates the UI. + */ private void loadProducts() { //Load all products from the backend using viewModel viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -148,6 +168,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc }); } + /** + * Navigates to the product detail screen for a specific product or to add a new one. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -161,6 +184,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } + /** + * Handles item click in the product list. + */ @Override public void onProductClick(int position) { openDetail(position); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index a7b16298..d77331ca 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -35,12 +35,18 @@ public class ProductSupplierFragment extends Fragment private EditText etSearch; private ProductSupplierViewModel viewModel; + /** + * Initializes the fragment and its associated ProductSupplierViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); } + /** + * Sets up the fragment's UI components, including the RecyclerView, search, and swipe-to-refresh. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -68,6 +74,9 @@ public class ProductSupplierFragment extends Fragment return view; } + /** + * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. + */ private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewPS); adapter = new ProductSupplierAdapter(filteredList, this); @@ -75,6 +84,9 @@ public class ProductSupplierFragment extends Fragment rv.setAdapter(adapter); } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchPS); etSearch.addTextChangedListener(new TextWatcher() { @@ -86,11 +98,17 @@ public class ProductSupplierFragment extends Fragment }); } + /** + * Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data. + */ private void setupSwipeRefresh(View view) { swipeRefresh = view.findViewById(R.id.swipeRefreshPS); swipeRefresh.setOnRefreshListener(this::loadData); } + /** + * Filters the product-supplier list based on the search query. + */ private void filter(String query) { filteredList.clear(); if (query.isEmpty()) { @@ -107,7 +125,9 @@ public class ProductSupplierFragment extends Fragment adapter.notifyDataSetChanged(); } - // Helper function to get a list of all product suppliers from the backend + /** + * Fetches all product-supplier data from the server through the ViewModel. + */ private void loadData() { //Load all product suppliers from the backend using viewModel viewModel.getAllProductSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -138,6 +158,9 @@ public class ProductSupplierFragment extends Fragment }); } + /** + * Navigates to the product-supplier detail screen for a specific item or to add a new record. + */ private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { @@ -151,6 +174,9 @@ public class ProductSupplierFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } + /** + * Handles item click in the product-supplier list. + */ @Override public void onProductSupplierClick(int position) { openDetail(position); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 35366e83..36050661 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -34,12 +34,18 @@ public class PurchaseOrderFragment extends Fragment private EditText etSearch; private PurchaseOrderViewModel viewModel; + /** + * Initializes the fragment and its associated PurchaseOrderViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); } + /** + * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,6 +70,9 @@ public class PurchaseOrderFragment extends Fragment return view; } + /** + * Initializes the RecyclerView with a layout manager and adapter for purchase orders. + */ private void setupRecyclerView(View view) { RecyclerView rv = view.findViewById(R.id.recyclerViewPO); adapter = new PurchaseOrderAdapter(filteredList, this); @@ -71,6 +80,9 @@ public class PurchaseOrderFragment extends Fragment rv.setAdapter(adapter); } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchPO); etSearch.addTextChangedListener(new TextWatcher() { @@ -86,11 +98,17 @@ public class PurchaseOrderFragment extends Fragment }); } + /** + * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. + */ private void setupSwipeRefresh(View view) { swipeRefresh = view.findViewById(R.id.swipeRefreshPO); swipeRefresh.setOnRefreshListener(this::loadData); } + /** + * Filters the purchase order list based on the search query. + */ private void filter(String query) { filteredList.clear(); if (query.isEmpty()) { @@ -107,7 +125,9 @@ public class PurchaseOrderFragment extends Fragment adapter.notifyDataSetChanged(); } - // Helper function to get a list of all purchase orders from the backend + /** + * Fetches all purchase order data from the server through the ViewModel and updates the UI. + */ private void loadData() { //Load all purchase orders from the backend using viewModel viewModel.getAllPurchaseOrders(0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -138,6 +158,9 @@ public class PurchaseOrderFragment extends Fragment }); } + /** + * Navigates to the purchase order detail screen for a specific record. + */ private void openDetail(int position) { Bundle args = new Bundle(); PurchaseOrderDTO po = filteredList.get(position); @@ -148,6 +171,9 @@ public class PurchaseOrderFragment extends Fragment NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } + /** + * Handles item click in the purchase order list. + */ @Override public void onPurchaseOrderClick(int position) { openDetail(position); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 1c12cbf8..ff0819b2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -45,13 +45,18 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + /** + * Initializes the fragment and its associated ServiceViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); } - //load service view + /** + * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -82,6 +87,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic return view; } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchService); etSearch.addTextChangedListener(new TextWatcher() { @@ -93,6 +101,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic }); } + /** + * Filters the service list based on the search query across name and description fields. + */ private void filterServices(String query) { filteredList.clear(); if (query.isEmpty()) { @@ -109,12 +120,17 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic adapter.notifyDataSetChanged(); } + /** + * Sets up the SwipeRefreshLayout to allow manual reloading of service data. + */ private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService); swipeRefreshLayout.setOnRefreshListener(this::loadServiceData); } - //Open the service detail view depending on the mode + /** + * Navigates to the service detail screen for editing an existing service or adding a new one. + */ private void openServiceDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); @@ -133,13 +149,17 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); } - // Called by ServiceAdapter when a row is clicked to open the details view + /** + * Handles item click in the service list. + */ @Override public void onServiceClick(int position) { openServiceDetails(position); } - // Helper function to get a list of all services from the backend + /** + * Fetches all service data from the server through the ViewModel and updates the UI. + */ private void loadServiceData() { //Load all services from the backend using viewModel viewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -172,7 +192,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic }); } - //set up the recyclerview and adapter + /** + * Initializes the RecyclerView with a layout manager and adapter for services. + */ private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices); adapter = new ServiceAdapter(filteredList, this); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 5e26a432..baa67b5b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -45,13 +45,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private SwipeRefreshLayout swipeRefreshLayout; private EditText etSearch; + /** + * Initializes the fragment and its associated SupplierViewModel. + */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); } - //load supplier view + /** + * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -82,6 +87,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp return view; } + /** + * Configures the search bar for filtering. + */ private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchSupplier); etSearch.addTextChangedListener(new TextWatcher() { @@ -93,6 +101,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp }); } + /** + * Filters the supplier list based on the search query across company name and contact person. + */ private void filterSuppliers(String query) { filteredList.clear(); if (query.isEmpty()) { @@ -110,12 +121,17 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp adapter.notifyDataSetChanged(); } + /** + * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. + */ private void setupSwipeRefresh(View view) { swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier); swipeRefreshLayout.setOnRefreshListener(this::loadSupplierData); } - //Open the supplier detail view depending on the mode + /** + * Navigates to the supplier detail screen for editing an existing record or adding a new one. + */ private void openSupplierDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); @@ -136,13 +152,17 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } - // Called by SupplierAdapter when a row is clicked to open the details view + /** + * Handles item click in the supplier list. + */ @Override public void onSupplierClick(int position) { openSupplierDetails(position); } - // Helper function to get a list of all suppliers from the backend + /** + * Fetches all supplier data from the server through the ViewModel and updates the UI. + */ private void loadSupplierData() { //Load all suppliers from the backend using viewModel viewModel.getAllSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { @@ -175,7 +195,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp }); } - //set up the recyclerview and adapter + /** + * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. + */ private void setupRecyclerView(View view) { RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers); adapter = new SupplierAdapter(filteredList, this); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index a9167678..a135e36e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -14,7 +14,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.ErrorUtils; + import java.util.*; import javax.inject.Inject; @@ -22,6 +23,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +/** + * Fragment for displaying and editing adoption request details. + */ @AndroidEntryPoint public class AdoptionDetailFragment extends Fragment { @@ -60,6 +64,9 @@ public class AdoptionDetailFragment extends Fragment { return view; } + /** + * Initializes UI components from the layout. + */ private void initViews(View v) { tvMode = v.findViewById(R.id.tvAdoptionMode); tvAdoptionId = v.findViewById(R.id.tvAdoptionId); @@ -72,11 +79,17 @@ public class AdoptionDetailFragment extends Fragment { btnBack = v.findViewById(R.id.btnAdoptionBack); } + /** + * Configures the spinner for adoption status. + */ private void setupSpinners() { spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, STATUSES)); } + /** + * Configures the date picker dialog for the adoption date field. + */ private void setupDatePicker() { etAdoptionDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); @@ -89,11 +102,17 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Fetches required data (pets and customers) from the backend. + */ private void loadData() { loadPets(); loadCustomers(); } + /** + * Loads the list of pets from the API. + */ private void loadPets() { petApi.getAllPets(0, 200) .enqueue(new Callback>() { @@ -110,6 +129,9 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Populates the pet selection spinner. + */ private void populatePetSpinner() { List names = new ArrayList<>(); names.add("-- Select Pet --"); @@ -125,6 +147,9 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * Loads the list of customers from the API. + */ private void loadCustomers() { customerApi.getAllCustomers(0, 200) .enqueue(new Callback>() { @@ -141,6 +166,9 @@ public class AdoptionDetailFragment extends Fragment { }); } + /** + * Populates the customer selection spinner. + */ private void populateCustomerSpinner() { List names = new ArrayList<>(); names.add("-- Select Customer --"); @@ -157,6 +185,9 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * Handles arguments to determine if the fragment is in edit or add mode. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { @@ -185,6 +216,9 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * Validates input and saves the adoption request to the backend. + */ private void saveAdoption() { if (spinnerCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; @@ -219,6 +253,9 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * callback for adoption save/update operations. + */ private Callback simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response r) { @@ -227,13 +264,7 @@ public class AdoptionDetailFragment extends Fragment { Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); navigateBack(); } else { - try { - String err = r.errorBody().string(); - Log.e("ADOPTION_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Log.e("ADOPTION_SAVE", "Failed to read error"); - } + ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code()); } } public void onFailure(Call c, Throwable t) { @@ -243,15 +274,16 @@ public class AdoptionDetailFragment extends Fragment { }; } + /** + * Shows a confirmation dialog before deleting an adoption request. + */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete Adoption?") .setPositiveButton("Yes", (d, w) -> adoptionApi.deleteAdoption(adoptionId) .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - navigateBack(); - } + public void onResponse(Call c, Response r) { navigateBack(); } public void onFailure(Call c, Throwable t) { Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); @@ -260,6 +292,9 @@ public class AdoptionDetailFragment extends Fragment { .setNegativeButton("No", null).show(); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 45ffe85a..0f192dce 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -14,7 +14,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.ErrorUtils; + import java.util.*; import javax.inject.Inject; @@ -22,6 +23,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +/** + * Fragment for displaying and editing appointment details. + */ @AndroidEntryPoint public class AppointmentDetailFragment extends Fragment { @@ -69,6 +73,9 @@ public class AppointmentDetailFragment extends Fragment { return view; } + /** + * Initializes UI components from the layout. + */ private void initViews(View v) { tvMode = v.findViewById(R.id.tvApptMode); tvAppointmentId = v.findViewById(R.id.tvAppointmentId); @@ -85,6 +92,9 @@ public class AppointmentDetailFragment extends Fragment { btnBack = v.findViewById(R.id.btnApptBack); } + /** + * Configures the adapters for spinners. + */ private void setupSpinners() { spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, STATUSES)); @@ -98,6 +108,9 @@ public class AppointmentDetailFragment extends Fragment { android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"})); } + /** + * Configures the date picker dialog for the appointment date field. + */ private void setupDatePicker() { etAppointmentDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); @@ -111,6 +124,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Fetches all required data from the backend to populate the fragment. + */ private void loadData() { loadPets(); loadServices(); @@ -119,6 +135,9 @@ public class AppointmentDetailFragment extends Fragment { loadAllAppointments(); } + /** + * Loads the list of pets from the API. + */ private void loadPets() { petApi.getAllPets(0, 200) .enqueue(new Callback>() { @@ -134,6 +153,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the pet selection spinner. + */ private void populatePetSpinner() { List names = new ArrayList<>(); names.add("-- Select Pet --"); @@ -149,6 +171,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Loads the list of services from the API. + */ private void loadServices() { serviceApi.getAllServices(0, 200) .enqueue(new Callback>() { @@ -164,6 +189,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the service selection spinner. + */ private void populateServiceSpinner() { List names = new ArrayList<>(); names.add("-- Select Service --"); @@ -179,6 +207,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Loads the list of customers from the API. + */ private void loadCustomers() { customerApi.getAllCustomers(0, 200) .enqueue(new Callback>() { @@ -194,6 +225,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the customer spinner. + */ private void populateCustomerSpinner() { List names = new ArrayList<>(); names.add("-- Select Customer --"); @@ -210,6 +244,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Loads the list of stores from the API. + */ private void loadStores() { storeApi.getAllStores(0, 50) .enqueue(new Callback>() { @@ -225,6 +262,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Populates the store spinner. + */ private void populateStoreSpinner() { List names = new ArrayList<>(); names.add("-- Select Store --"); @@ -240,6 +280,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Loads all appointments from the API. + */ private void loadAllAppointments() { appointmentApi.getAllAppointments(0, 500) .enqueue(new Callback>() { @@ -251,6 +294,9 @@ public class AppointmentDetailFragment extends Fragment { }); } + /** + * Handles arguments to determine if the fragment is in edit or add mode. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { @@ -292,6 +338,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Validates input and saves the appointment to the backend. + */ private void saveAppointment() { if (spinnerCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; @@ -370,6 +419,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * callback for appointment save/update operations. + */ private Callback simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response r) { @@ -387,22 +439,12 @@ public class AppointmentDetailFragment extends Fragment { showErrorDialog("Invalid Date/Time", "Booked appointments must be scheduled in the future. " + "Please select a future date and time."); - //------------------------------------------ } else if (errorBody.toLowerCase().contains("not available") || errorBody.toLowerCase().contains("time is not available")) { showNoAvailabilityDialog(); - } else if (r.code() == 404) { - showErrorDialog("Not Found", - "The selected pet, customer or service was not found."); - } else if (r.code() == 403) { - showErrorDialog("Access Denied", - "You don't have permission to perform this action."); - } else if (r.code() == 400) { - showErrorDialog("Invalid Request", errorBody); } else { - showErrorDialog("Error", "Something went wrong. Please try again."); + ErrorUtils.showErrorMessage(getContext(), r, "Something went wrong. Please try again."); } - //----------------------------- } catch (Exception e) { Log.e("APPT_SAVE", "Failed to read error body"); showErrorDialog("Error", "Something went wrong. Please try again."); @@ -417,6 +459,9 @@ public class AppointmentDetailFragment extends Fragment { }; } + /** + * Shows a specialized dialog when a time slot is not available. + */ private void showNoAvailabilityDialog() { new AlertDialog.Builder(requireContext()) .setTitle("No Availability") @@ -427,6 +472,9 @@ public class AppointmentDetailFragment extends Fragment { .show(); } + /** + * Shows a generic error dialog with a title and message. + */ private void showErrorDialog(String title, String message) { new AlertDialog.Builder(requireContext()) .setTitle(title) @@ -434,6 +482,10 @@ public class AppointmentDetailFragment extends Fragment { .setPositiveButton("OK", null) .show(); } + + /** + * Shows a confirmation dialog and handles the deletion of an appointment. + */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete Appointment?") @@ -448,6 +500,9 @@ public class AppointmentDetailFragment extends Fragment { .setNegativeButton("No", null).show(); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 92fecd47..ee22a536 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -26,8 +26,8 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.fragments.listfragments.InventoryFragment; +import com.example.petstoremobile.utils.ErrorUtils; import java.util.ArrayList; import java.util.List; @@ -39,6 +39,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Fragment for displaying and editing inventory item details. + */ @AndroidEntryPoint public class InventoryDetailFragment extends Fragment { @@ -65,6 +68,9 @@ public class InventoryDetailFragment extends Fragment { private final List productSuggestions = new ArrayList<>(); private ArrayAdapter dropdownAdapter; + /** + * Sets the parent inventory fragment to notify of changes. + */ public void setInventoryFragment(InventoryFragment fragment) { this.inventoryFragment = fragment; } @@ -85,6 +91,9 @@ public class InventoryDetailFragment extends Fragment { return view; } + /** + * get the layout view and set adapter. + */ private void initViews(View view) { tvMode = view.findViewById(R.id.tvInventoryMode); tvInventoryId = view.findViewById(R.id.tvInventoryId); @@ -102,7 +111,9 @@ public class InventoryDetailFragment extends Fragment { etProductSearch.setThreshold(1); // start showing after 1 character } - // Product search dropdown + /** + * setup the product search dropdown. + */ private void setupProductSearch() { etProductSearch.addTextChangedListener(new TextWatcher() { @Override @@ -143,6 +154,9 @@ public class InventoryDetailFragment extends Fragment { }); } + /** + * Searches for products matching the query from the backend. + */ private void searchProducts(String query) { productApi.getAllProducts(query, 0, 20).enqueue(new Callback>() { @Override @@ -172,8 +186,9 @@ public class InventoryDetailFragment extends Fragment { }); } - // Arguments (edit mode) - + /** + * arguments to set up edit or add mode. + */ private void handleArguments() { Bundle args = getArguments(); if (args != null && args.containsKey("inventoryId")) { @@ -214,7 +229,9 @@ public class InventoryDetailFragment extends Fragment { } } - // Save + /** + * Saves the current inventory item details to the backend. + */ private void saveInventory() { if (selectedProduct == null) { etProductSearch.setError("Please select a product from the list"); @@ -255,7 +272,7 @@ public class InventoryDetailFragment extends Fragment { Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); notifyParentAndGoBack(); } else { - Toast.makeText(getContext(), "Update failed: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Update failed"); } } @@ -274,7 +291,7 @@ public class InventoryDetailFragment extends Fragment { Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); notifyParentAndGoBack(); } else { - Toast.makeText(getContext(), "Create failed: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Create failed"); } } @@ -287,7 +304,9 @@ public class InventoryDetailFragment extends Fragment { } } - // Delete + /** + * Shows a confirmation dialog before deleting an inventory item. + */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete inventory item?") @@ -297,6 +316,9 @@ public class InventoryDetailFragment extends Fragment { .show(); } + /** + * Sends a request to the API to delete the inventory item. + */ private void deleteInventory() { setButtonsEnabled(false); inventoryApi.deleteInventory(inventoryId).enqueue(new Callback() { @@ -307,7 +329,7 @@ public class InventoryDetailFragment extends Fragment { Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); notifyParentAndGoBack(); } else { - Toast.makeText(getContext(), "Delete failed: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Delete failed"); } } @@ -319,18 +341,25 @@ public class InventoryDetailFragment extends Fragment { }); } - // Helpers - + /** + * Notifies the parent fragment of a change and navigates back. + */ private void notifyParentAndGoBack() { if (inventoryFragment != null) inventoryFragment.onInventoryChanged(); navigateBack(); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } + /** + * Enables or disables action buttons. + */ private void setButtonsEnabled(boolean enabled) { btnSave.setEnabled(enabled); btnDelete.setEnabled(enabled); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 8aa3b319..83ce6d51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -3,9 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -13,7 +11,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.Spinner; @@ -24,9 +21,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; -import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.PetFragment; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; import javax.inject.Inject; @@ -36,6 +32,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Fragment for displaying and editing pet details. + */ @AndroidEntryPoint public class PetDetailFragment extends Fragment { @@ -66,7 +65,9 @@ public class PetDetailFragment extends Fragment { return view; } - //Method to Update or Add a pet + /** + * Handles the saving of pet data (adding/updating). + */ private void savePet() { // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; @@ -104,7 +105,7 @@ public class PetDetailFragment extends Fragment { Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to update pet"); } } @@ -112,7 +113,7 @@ public class PetDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t)); Log.e("PetDetailFragment", "Error updating pet", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } else { @@ -125,7 +126,7 @@ public class PetDetailFragment extends Fragment { Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to add pet"); } } @@ -133,13 +134,15 @@ public class PetDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t)); Log.e("PetDetailFragment", "Error adding pet", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } } - //Method to Delete a pet + /** + * Displays a confirmation dialog and handles the deletion of a pet. + */ private void deletePet() { //Alert the user to confirm the delete new AlertDialog.Builder(requireContext()) @@ -155,7 +158,7 @@ public class PetDetailFragment extends Fragment { Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet"); } } @@ -163,7 +166,7 @@ public class PetDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t)); Log.e("PetDetailFragment", "Error deleting pet", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); }) @@ -171,12 +174,16 @@ public class PetDetailFragment extends Fragment { .show(); } - //Helper method to navigate back to the list + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - //helper function to check if pet is being edited or added and show the view accordingly + /** + * Handles arguments passed to the fragment to determine if it's in edit or add mode. + */ private void handleArguments() { // Pet is being edited if the bundle contains a petId if (getArguments() != null && getArguments().containsKey("petId")) { @@ -208,7 +215,9 @@ public class PetDetailFragment extends Fragment { } } - //helper function to get controls from layout + /** + * Binds UI components from the layout. + */ private void initViews(View view) { tvMode = view.findViewById(R.id.tvMode); tvPetId = view.findViewById(R.id.tvPetId); @@ -223,7 +232,9 @@ public class PetDetailFragment extends Fragment { btnBack = view.findViewById(R.id.btnBack); } - //helper function to set up the spinner menu for pet status + /** + * Initializes the spinner for pet status selection. + */ private void setupSpinner() { BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 0eecece6..8baa9646 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -1,38 +1,25 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; -import android.provider.MediaStore; -import android.util.Log; import android.view.*; import android.widget.*; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.ImagePickerHelper; import java.io.File; import java.math.BigDecimal; @@ -46,6 +33,9 @@ import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; +/** + * Fragment for displaying and editing product details, including image selection. + */ @AndroidEntryPoint public class ProductDetailFragment extends Fragment { @@ -65,52 +55,43 @@ public class ProductDetailFragment extends Fragment { private List categoryList = new ArrayList<>(); private Uri photoUri; private ProductViewModel viewModel; + private ImagePickerHelper imagePickerHelper; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - private ActivityResultLauncher galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher permissionLauncher; - + /** + * Initializes activity launchers and the ImagePickerHelper. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductViewModel.class); - galleryLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - Uri selectedImage = result.getData().getData(); - Glide.with(this).load(selectedImage).into(ivProductImage); - photoUri = selectedImage; - hasImage = true; - isImageChanged = true; - isImageRemoved = false; - } - } - ); - cameraLauncher = registerForActivityResult( - new ActivityResultContracts.TakePicture(), - success -> { - if (success) { - Glide.with(this).load(photoUri).into(ivProductImage); - hasImage = true; - isImageChanged = true; - isImageRemoved = false; - } - } - ); - permissionLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), - granted -> { - if (granted) launchCamera(); - else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show(); - } - ); + imagePickerHelper = new ImagePickerHelper(this, "product_photo.jpg", new ImagePickerHelper.ImagePickerListener() { + @Override + public void onImagePicked(Uri uri) { + photoUri = uri; + Glide.with(ProductDetailFragment.this).load(uri).into(ivProductImage); + hasImage = true; + isImageChanged = true; + isImageRemoved = false; + } + + @Override + public void onImageRemoved() { + photoUri = null; + hasImage = false; + isImageChanged = false; + isImageRemoved = true; + ivProductImage.setImageResource(R.drawable.placeholder2); + } + }); } + /** + * Inflates the layout and initializes UI components and listeners. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -122,10 +103,13 @@ public class ProductDetailFragment extends Fragment { btnBack.setOnClickListener(v -> navigateBack()); btnSave.setOnClickListener(v -> saveProduct()); btnDelete.setOnClickListener(v -> confirmDelete()); - ivProductImage.setOnClickListener(v -> showImagePickerDialog()); + ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); return view; } + /** + * get the UI components from the layout. + */ private void initViews(View v) { tvMode = v.findViewById(R.id.tvProductMode); tvProductId = v.findViewById(R.id.tvProductId); @@ -139,63 +123,21 @@ public class ProductDetailFragment extends Fragment { ivProductImage = v.findViewById(R.id.ivProductImage); } - // Helper function to show the image picker dialog - private void showImagePickerDialog() { - List options = new ArrayList<>(); - options.add("Take Photo"); - options.add("Choose from Gallery"); - if (hasImage) { - options.add("Remove Photo"); - } - - new AlertDialog.Builder(requireContext()) - .setTitle("Select Product Image") - .setItems(options.toArray(new String[0]), (dialog, which) -> { - String selectedOption = options.get(which); - if (selectedOption.equals("Take Photo")) { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED) { - launchCamera(); - } else { - permissionLauncher.launch(Manifest.permission.CAMERA); - } - } else if (selectedOption.equals("Choose from Gallery")) { - Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - galleryLauncher.launch(intent); - } else if (selectedOption.equals("Remove Photo")) { - removePhoto(); - } - }) - .show(); - } - - // Helper function to remove the photo locally - private void removePhoto() { - photoUri = null; - hasImage = false; - isImageChanged = false; - isImageRemoved = true; - Glide.with(this).load(R.drawable.placeholder2).into(ivProductImage); - } - - // Helper function to launch the camera - private void launchCamera() { - File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg"); - photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); - cameraLauncher.launch(photoUri); - } - - // Helper function to load categories from the backend for the spinner + /** + * Fetches all product categories for the selection spinner. + */ private void loadCategories() { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { categoryList = resource.data.getContent(); populateCategorySpinner(); } }); } - // Helper function to populate the category spinner + /** + * Fills the spinner with category names. + */ private void populateCategorySpinner() { List names = new ArrayList<>(); names.add("-- Select Category --"); @@ -211,6 +153,9 @@ public class ProductDetailFragment extends Fragment { } } + /** + * Checks if the fragment was opened with existing product data for editing. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("prodId")) { @@ -235,34 +180,34 @@ public class ProductDetailFragment extends Fragment { } } - //load the product image from the backend + /** + * Loads the product image from the backend. + */ private void loadProductImage() { String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); String token = tokenManager.getToken(); - Object loadTarget = imageUrl; - if (token != null) { - loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() - .addHeader("Authorization", "Bearer " + token) - .build()); - } + GlideUtils.loadImageWithToken(requireContext(), ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } - Glide.with(this) - .load(loadTarget) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .placeholder(R.drawable.placeholder2) - .error(R.drawable.placeholder2) - .into(ivProductImage); + @Override + public void onLoadFailed() { + hasImage = false; + } + }); } - // Function to check any changes to the image and perform the appropriate action - // updating/adding photo, removing photo or no change + /** + * Performs image related actions (upload/delete) after product details are saved. + */ private void performPendingImageActions(String successMsg) { if (isImageRemoved) { viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { + if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { + if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); @@ -278,8 +223,9 @@ public class ProductDetailFragment extends Fragment { } } - // Helper function to upload the product image by calling the backend - // and then navigate back to the previous screen + /** + * Uploads the selected image file to the server. + */ private void uploadProductImageAndNavigate(Uri uri, String successMsg) { File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) { @@ -292,8 +238,8 @@ public class ProductDetailFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { + if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { + if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); @@ -303,6 +249,9 @@ public class ProductDetailFragment extends Fragment { }); } + /** + * Validates input fields and saves product information to the backend. + */ private void saveProduct() { String name = etProductName.getText().toString().trim(); String desc = etProductDesc.getText().toString().trim(); @@ -330,8 +279,8 @@ public class ProductDetailFragment extends Fragment { if (isEditing) { viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { + if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { + if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { performPendingImageActions("Updated"); } else { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); @@ -340,8 +289,8 @@ public class ProductDetailFragment extends Fragment { }); } else { viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { + if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { prodId = resource.data.getProdId(); performPendingImageActions("Saved"); } else { @@ -352,21 +301,26 @@ public class ProductDetailFragment extends Fragment { } } - // Function to delete the product from the server + /** + * Displays a confirmation dialog before deleting the product. + */ private void confirmDelete() { - new AlertDialog.Builder(requireContext()) + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("Delete Product?") .setPositiveButton("Yes", (d, w) -> viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS) { + if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { navigateBack(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { + } else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })) .setNegativeButton("No", null).show(); } + /** + * Navigates back to the previous fragment. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index d934f64f..f1559957 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -13,7 +13,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; -import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.ErrorUtils; + import java.math.BigDecimal; import java.util.*; @@ -22,6 +23,9 @@ import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; import retrofit2.*; +/** + * Fragment for displaying and editing the relationship between products and suppliers, + */ @AndroidEntryPoint public class ProductSupplierDetailFragment extends Fragment { @@ -57,6 +61,9 @@ public class ProductSupplierDetailFragment extends Fragment { return view; } + /** + * Initializes UI components from the layout. + */ private void initViews(View v) { tvMode = v.findViewById(R.id.tvPSMode); spinnerProduct = v.findViewById(R.id.spinnerPSProduct); @@ -67,11 +74,17 @@ public class ProductSupplierDetailFragment extends Fragment { btnBack = v.findViewById(R.id.btnPSBack); } + /** + * Fetches products and suppliers to populate the spinners. + */ private void loadData() { loadProducts(); loadSuppliers(); } + /** + * Loads the list of products from the API. + */ private void loadProducts() { productApi.getAllProducts(null, 0, 200) .enqueue(new Callback>() { @@ -88,6 +101,9 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Populates the product spinner. + */ private void populateProductSpinner() { List names = new ArrayList<>(); names.add("-- Select Product --"); @@ -103,6 +119,9 @@ public class ProductSupplierDetailFragment extends Fragment { } } + /** + * Loads the list of suppliers from the API. + */ private void loadSuppliers() { supplierApi.getAllSuppliers(0, 200) .enqueue(new Callback>() { @@ -119,6 +138,9 @@ public class ProductSupplierDetailFragment extends Fragment { }); } + /** + * Populates the supplier spinner. + */ private void populateSupplierSpinner() { List names = new ArrayList<>(); names.add("-- Select Supplier --"); @@ -134,6 +156,9 @@ public class ProductSupplierDetailFragment extends Fragment { } } + /** + * Handles arguments to determine if the fragment is in edit or add mode. + */ private void handleArguments() { Bundle a = getArguments(); if (a != null && a.containsKey("productId")) { @@ -151,6 +176,9 @@ public class ProductSupplierDetailFragment extends Fragment { } } + /** + * Validates input and saves the product-supplier to the backend. + */ private void save() { if (spinnerProduct.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; @@ -183,6 +211,9 @@ public class ProductSupplierDetailFragment extends Fragment { } } + /** + * callback for product-supplier save/update operations. + */ private Callback simpleCallback(String msg) { return new Callback<>() { public void onResponse(Call c, Response r) { @@ -190,13 +221,7 @@ public class ProductSupplierDetailFragment extends Fragment { Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); navigateBack(); } else { - try { - String err = r.errorBody().string(); - Log.e("PS_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Log.e("PS_SAVE", "Failed to read error"); - } + ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code()); } } public void onFailure(Call c, Throwable t) { @@ -206,6 +231,9 @@ public class ProductSupplierDetailFragment extends Fragment { }; } + /** + * Shows a confirmation dialog before deleting a product-supplier relationship. + */ private void confirmDelete() { new AlertDialog.Builder(requireContext()) .setTitle("Delete?") @@ -223,6 +251,9 @@ public class ProductSupplierDetailFragment extends Fragment { .setNegativeButton("No", null).show(); } + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index 38da2aa4..e4f05a26 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -9,16 +9,21 @@ import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.fragments.ListFragment; import dagger.hilt.android.AndroidEntryPoint; +/** + * Fragment for displaying the information of a purchase order. + */ @AndroidEntryPoint public class PurchaseOrderDetailFragment extends Fragment { private TextView tvId, tvSupplier, tvDate, tvStatus; private Button btnBack; + /** + * Inflates the layout, initializes views, and populates order data from arguments. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index fc65a1e1..0842f6bc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -20,6 +20,7 @@ import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.listfragments.ServiceFragment; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; import javax.inject.Inject; @@ -29,6 +30,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Fragment for displaying and editing service details. + */ @AndroidEntryPoint public class ServiceDetailFragment extends Fragment { @@ -41,7 +45,9 @@ public class ServiceDetailFragment extends Fragment { @Inject ServiceApi serviceApi; - //set the service fragment to the parent so we refer back to service view when save or delete is done + /** + * Sets the parent service fragment to notify of changes. + */ public void setServiceFragment(ServiceFragment fragment) { this.serviceFragment = fragment; } @@ -63,7 +69,9 @@ public class ServiceDetailFragment extends Fragment { return view; } - //Method to Update or Add a service + /** + * Handles the saving of service data (adding or updating). + */ private void saveService() { // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return; @@ -96,7 +104,7 @@ public class ServiceDetailFragment extends Fragment { Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to update service"); } } @@ -104,7 +112,7 @@ public class ServiceDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t)); Log.e("ServiceDetailFragment", "Error updating service", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } else { @@ -117,7 +125,7 @@ public class ServiceDetailFragment extends Fragment { Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to add service"); } } @@ -125,13 +133,15 @@ public class ServiceDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t)); Log.e("ServiceDetailFragment", "Error adding service", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } } - //Method to Delete a service + /** + * Displays a confirmation dialog and handles the deletion of a service. + */ private void deleteService() { //Alert the user to confirm the delete new AlertDialog.Builder(requireContext()) @@ -146,7 +156,7 @@ public class ServiceDetailFragment extends Fragment { Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service"); } } @@ -154,7 +164,7 @@ public class ServiceDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t)); Log.e("ServiceDetailFragment", "Error deleting service", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); }) @@ -162,12 +172,16 @@ public class ServiceDetailFragment extends Fragment { .show(); } - //Helper method to navigate back to the list + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - //helper function to check if service is being edited or added and show the view accordingly + /** + * Handles arguments passed to the fragment to determine if it's in edit or add mode. + */ private void handleArguments() { // Service is being edited if the bundle contains a serviceId if (getArguments() != null && getArguments().containsKey("serviceId")) { @@ -192,7 +206,9 @@ public class ServiceDetailFragment extends Fragment { } } - //helper function to get controls from layout + /** + * Set UI components from the layout. + */ private void initViews(View view) { tvMode = view.findViewById(R.id.tvMode); tvServiceId = view.findViewById(R.id.tvServiceId); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index e1651c1a..e4b41598 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -19,7 +19,9 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.UIUtils; import javax.inject.Inject; @@ -28,6 +30,9 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +/** + * Fragment for displaying and editing supplier details. + */ @AndroidEntryPoint public class SupplierDetailFragment extends Fragment { @@ -56,7 +61,9 @@ public class SupplierDetailFragment extends Fragment { return view; } - //Method to Update or Add a supplier + /** + * Handles the saving of supplier data (adding or updating). + */ private void saveSupplier() { // Validates all fields using InputValidator if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return; @@ -92,7 +99,7 @@ public class SupplierDetailFragment extends Fragment { Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to update supplier"); } } @@ -100,7 +107,7 @@ public class SupplierDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t)); Log.e("SupplierDetailFragment", "Error updating supplier", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } else { @@ -113,7 +120,7 @@ public class SupplierDetailFragment extends Fragment { Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to add supplier"); } } @@ -121,13 +128,15 @@ public class SupplierDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t)); Log.e("SupplierDetailFragment", "Error adding supplier", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); } } - //Method to Delete a supplier + /** + * Displays a confirmation dialog and handles the deletion of a supplier. + */ private void deleteSupplier() { //Alert the user to confirm the delete new AlertDialog.Builder(requireContext()) @@ -142,7 +151,7 @@ public class SupplierDetailFragment extends Fragment { Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else { - Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier"); } } @@ -150,7 +159,7 @@ public class SupplierDetailFragment extends Fragment { public void onFailure(Call call, Throwable t) { ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t)); Log.e("SupplierDetailFragment", "Error deleting supplier", t); - Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); } }); }) @@ -158,12 +167,16 @@ public class SupplierDetailFragment extends Fragment { .show(); } - //Helper method to navigate back to the list + /** + * Navigates back to the previous screen. + */ private void navigateBack() { NavHostFragment.findNavController(this).popBackStack(); } - //helper function to check if supplier is being edited or added and show the view accordingly + /** + * Handles arguments passed to the fragment to determine if it's in edit or add mode. + */ private void handleArguments() { // Supplier is being edited if the bundle contains a supId if (getArguments() != null && getArguments().containsKey("supId")) { @@ -189,7 +202,9 @@ public class SupplierDetailFragment extends Fragment { } } - //helper function to get controls from layout + /** + * Initializes the UI components and sets up formatting for phone input. + */ private void initViews(View view) { tvMode = view.findViewById(R.id.tvMode); tvSupId = view.findViewById(R.id.tvSupId); @@ -200,8 +215,7 @@ public class SupplierDetailFragment extends Fragment { etSupPhone = view.findViewById(R.id.etSupPhone); // Add phone number formatting (CA) and limit length to 14 characters - etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA")); - etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)}); + UIUtils.formatPhoneInput(etSupPhone); btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier); btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 6e6943d1..57915e29 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -1,22 +1,11 @@ package com.example.petstoremobile.fragments.listfragments.listprofilefragments; -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Color; import android.net.Uri; import android.os.Bundle; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; -import android.provider.MediaStore; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -26,21 +15,14 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.ImagePickerHelper; import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import javax.inject.Inject; @@ -60,7 +42,6 @@ public class PetProfileFragment extends Fragment { private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; private Button btnBack, btnEditPet, btnChangePhoto; private ImageView imgPet; - private Uri photoUri; private int petId; private boolean hasImage = false; @@ -68,59 +49,32 @@ public class PetProfileFragment extends Fragment { @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; - // launchers for camera and gallery - private ActivityResultLauncher galleryLauncher; - private ActivityResultLauncher cameraLauncher; - private ActivityResultLauncher permissionLauncher; + private ImagePickerHelper imagePickerHelper; + /** + * Initializes activity launchers for gallery, camera, and permissions. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Launcher to open gallery to select image - galleryLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - Uri selectedImage = result.getData().getData(); - uploadPetImage(selectedImage); - } - } - ); + imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { + @Override + public void onImagePicked(Uri uri) { + uploadPetImage(uri); + } - // Launcher for camera to open and capture image - cameraLauncher = registerForActivityResult( - new ActivityResultContracts.TakePicture(), - success -> { - if (success) { - uploadPetImage(photoUri); - } - } - ); - - // Launcher to request camera permission - permissionLauncher = registerForActivityResult( - new ActivityResultContracts.RequestPermission(), - granted -> { - if (granted) { - launchCamera(); - } else { - new AlertDialog.Builder(requireContext()) - .setTitle("Permission Required") - .setMessage("Please grant camera permission to use this feature") - .setPositiveButton("Open Settings", (dialog, which) -> { - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null)); - startActivity(intent); - }) - .setNegativeButton("Cancel", null) - .show(); - } - } - ); + @Override + public void onImageRemoved() { + deletePetImage(); + } + }); } + /** + * Inflates the layout, initializes views, and sets up click listeners. + */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -164,78 +118,38 @@ public class PetProfileFragment extends Fragment { //Make change photo button ask user to select a new photo btnChangePhoto.setOnClickListener(v -> { - List options = new ArrayList<>(); - options.add("Take Photo"); - options.add("Choose from Gallery"); - if (hasImage) { - options.add("Remove Photo"); - } - - new AlertDialog.Builder(requireContext()) - .setTitle("Change Pet Photo") - .setItems(options.toArray(new String[0]), (dialog, which) -> { - String selected = options.get(which); - if (selected.equals("Take Photo")) { - // Choose Camera - //Checks if the user has granted the camera permission already - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - //if the permission is already granted then launch the camera - launchCamera(); - } else { - //otherwise request the permission - permissionLauncher.launch(Manifest.permission.CAMERA); - } - } else if (selected.equals("Choose from Gallery")) { - Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); - galleryLauncher.launch(intent); - } else if (selected.equals("Remove Photo")) { - deletePetImage(); - } - }) - .show(); + imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); return view; } - // Helper function to load pet image from backend + /** + * Fetches and displays the pet's image from the server. + */ private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String token = tokenManager.getToken(); - Object loadTarget = imageUrl; - if (token != null) { - loadTarget = new GlideUrl(imageUrl, new LazyHeaders.Builder() - .addHeader("Authorization", "Bearer " + token) - .build()); - } + GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } - Glide.with(this) - .load(loadTarget) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .placeholder(R.drawable.placeholder) - .error(R.drawable.placeholder) - .listener(new com.bumptech.glide.request.RequestListener() { - @Override - public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { - hasImage = false; - return false; - } - - @Override - public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { - hasImage = true; - return false; - } - }) - .into(imgPet); + @Override + public void onLoadFailed() { + hasImage = false; + } + }); } - // Helper function to upload pet image to backend + /** + * Uploads a selected or captured image a pet photo through the API. + */ private void uploadPetImage(Uri uri) { try { - File file = getFileFromUri(uri); + File file = FileUtils.getFileFromUri(requireContext(), uri); if (file == null) return; // Create RequestBody for file upload @@ -266,6 +180,9 @@ public class PetProfileFragment extends Fragment { } } + /** + * Sends a request to the API to remove the current pet photo. + */ private void deletePetImage() { petApi.deletePetImage((long) petId).enqueue(new Callback() { @Override @@ -286,30 +203,4 @@ public class PetProfileFragment extends Fragment { } }); } - - // Helper function to create a temporary File object from a Uri for uploading - private File getFileFromUri(Uri uri) { - try { - InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); - File tempFile = new File(requireContext().getCacheDir(), "upload_pet_image.jpg"); - FileOutputStream outputStream = new FileOutputStream(tempFile); - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, length); - } - outputStream.close(); - inputStream.close(); - return tempFile; - } catch (Exception e) { - Log.e("FILE_UTILS", "Error creating temp file", e); - return null; - } - } - - private void launchCamera() { - File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg"); - photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); - cameraLauncher.launch(photoUri); - } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java new file mode 100644 index 00000000..941f9662 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -0,0 +1,32 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; +import com.example.petstoremobile.dtos.ErrorResponse; +import com.google.gson.Gson; +import retrofit2.Response; + +/** + * Utility class for handling API error responses. + */ +public class ErrorUtils { + /** + * Shows an error message to toast based on the response. + */ + public static void showErrorMessage(Context context, Response response, String defaultMessage) { + try { + if (response.errorBody() != null) { + String errorJson = response.errorBody().string(); + ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class); + if (errorResponse != null && errorResponse.getMessage() != null) { + Toast.makeText(context, errorResponse.getMessage(), Toast.LENGTH_LONG).show(); + return; + } + } + } catch (Exception e) { + Log.e("ErrorUtils", "Error parsing error body", e); + } + Toast.makeText(context, defaultMessage, Toast.LENGTH_SHORT).show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java new file mode 100644 index 00000000..d5810dc1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/GlideUtils.java @@ -0,0 +1,122 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.widget.ImageView; +import androidx.annotation.Nullable; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.example.petstoremobile.R; + +/** + * Utility class for loading images using Glide with authentication tokens. + */ +public class GlideUtils { + + /** + * interface to check the status of the image load. + */ + public interface ImageLoadListener { + void onResourceReady(); + void onLoadFailed(); + } + + /** + * Loads an image from a URL into an ImageView with token. + */ + public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder) { + loadImageWithToken(context, imageView, url, token, placeholder, null); + } + + /** + * Loads an image from a URL into an ImageView with token and listener. + */ + public static void loadImageWithToken(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) { + if (url == null) { + imageView.setImageResource(placeholder); + if (listener != null) listener.onLoadFailed(); + return; + } + + Object loadTarget = url; + if (token != null && url.startsWith("http")) { + loadTarget = new GlideUrl(url, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + } + + Glide.with(context) + .load(loadTarget) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(placeholder) + .error(placeholder) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (listener != null) listener.onLoadFailed(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (listener != null) listener.onResourceReady(); + return false; + } + }) + .into(imageView); + } + + /** + * Loads an image from a URL into an ImageView with token and applies circle cropping for image. + */ + public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder) { + loadImageWithTokenCircle(context, imageView, url, token, placeholder, null); + } + + /** + * Loads an image from a URL into an ImageView with token, circle cropping, and listener. + */ + public static void loadImageWithTokenCircle(Context context, ImageView imageView, String url, String token, int placeholder, ImageLoadListener listener) { + if (url == null) { + imageView.setImageResource(placeholder); + if (listener != null) listener.onLoadFailed(); + return; + } + + Object loadTarget = url; + if (token != null && url.startsWith("http")) { + loadTarget = new GlideUrl(url, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + } + + Glide.with(context) + .load(loadTarget) + .circleCrop() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(placeholder) + .error(placeholder) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (listener != null) listener.onLoadFailed(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (listener != null) listener.onResourceReady(); + return false; + } + }) + .into(imageView); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java new file mode 100644 index 00000000..4d1e9bf9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ImagePickerHelper.java @@ -0,0 +1,160 @@ +package com.example.petstoremobile.utils; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.MediaStore; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to handle image picking from camera or gallery. + */ +public class ImagePickerHelper { + + /** + * Listener interface to handle the results of image picking. + */ + public interface ImagePickerListener { + /** + * Called when an image has been successfully selected or captured. + */ + void onImagePicked(Uri uri); + /** + * Called when the user chooses to remove the existing image. + */ + void onImageRemoved(); + } + + private final Fragment fragment; + private final ImagePickerListener listener; + private final ActivityResultLauncher galleryLauncher; + private final ActivityResultLauncher cameraLauncher; + private final ActivityResultLauncher permissionLauncher; + private Uri photoUri; + private final String tempFileName; + + /** + * Constructor for ImagePickerHelper. + * Registers activity launchers for gallery, camera, and permissions. + */ + public ImagePickerHelper(Fragment fragment, String tempFileName, ImagePickerListener listener) { + this.fragment = fragment; + this.tempFileName = tempFileName; + this.listener = listener; + + // Launcher to open gallery to select image + galleryLauncher = fragment.registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri selectedImage = result.getData().getData(); + if (selectedImage != null) { + listener.onImagePicked(selectedImage); + } + } + } + ); + + // Launcher for camera to open and capture image + cameraLauncher = fragment.registerForActivityResult( + new ActivityResultContracts.TakePicture(), + success -> { + if (success && photoUri != null) { + listener.onImagePicked(photoUri); + } + } + ); + + // Launcher to request camera permission + permissionLauncher = fragment.registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> { + if (granted) { + launchCamera(); + } else { + showPermissionDeniedDialog(); + } + } + ); + } + + /** + * Shows a dialog to choose between camera, gallery, and optionally remove photo. + */ + public void showImagePickerDialog(String title, boolean hasImage) { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + + new AlertDialog.Builder(fragment.requireContext()) + .setTitle(title) + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selected = options.get(which); + if (selected.equals("Take Photo")) { + checkCameraPermission(); + } else if (selected.equals("Choose from Gallery")) { + launchGallery(); + } else if (selected.equals("Remove Photo")) { + listener.onImageRemoved(); + } + }) + .show(); + } + + /** + * Checks if camera permission is granted and launches camera or requests permission. + */ + private void checkCameraPermission() { + if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + launchCamera(); + } else { + permissionLauncher.launch(Manifest.permission.CAMERA); + } + } + + /** + * Prepares a temporary file and launches the camera app. + */ + private void launchCamera() { + File photoFile = new File(fragment.requireContext().getCacheDir(), tempFileName); + photoUri = FileProvider.getUriForFile(fragment.requireContext(), fragment.requireContext().getPackageName() + ".fileprovider", photoFile); + cameraLauncher.launch(photoUri); + } + + /** + * Launches the gallery app to select an existing image. + */ + private void launchGallery() { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } + + /** + * Shows a dialog explaining why camera permission is needed when denied. + */ + private void showPermissionDeniedDialog() { + new AlertDialog.Builder(fragment.requireContext()) + .setTitle("Permission Required") + .setMessage("Please grant camera permission to use this feature") + .setPositiveButton("Open Settings", (dialog, which) -> { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", fragment.requireContext().getPackageName(), null)); + fragment.startActivity(intent); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java new file mode 100644 index 00000000..22b8d4e0 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/UIUtils.java @@ -0,0 +1,18 @@ +package com.example.petstoremobile.utils; + +import android.telephony.PhoneNumberFormattingTextWatcher; +import android.text.InputFilter; +import android.widget.EditText; + +/** + * Utility class for shared UI component logic and formatting. + */ +public class UIUtils { + /** + * Formats an EditText for to phone format + */ + public static void formatPhoneInput(EditText editText) { + editText.addTextChangedListener(new PhoneNumberFormattingTextWatcher("CA")); + editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(14)}); + } +} From e354592c473f9d513874cb3ab6b4175965be8450 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:48:14 -0600 Subject: [PATCH 060/137] remove dead code --- .../InventoryDetailFragment.java | 24 +++---------------- .../ServiceDetailFragment.java | 13 ++-------- .../viewmodels/PetViewModel.java | 2 -- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index ee22a536..a363e421 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -26,7 +26,6 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.fragments.listfragments.InventoryFragment; import com.example.petstoremobile.utils.ErrorUtils; import java.util.ArrayList; @@ -52,7 +51,6 @@ public class InventoryDetailFragment extends Fragment { @Inject InventoryApi inventoryApi; @Inject ProductApi productApi; - private InventoryFragment inventoryFragment; private boolean isEditing = false; private long inventoryId = -1; @@ -68,13 +66,6 @@ public class InventoryDetailFragment extends Fragment { private final List productSuggestions = new ArrayList<>(); private ArrayAdapter dropdownAdapter; - /** - * Sets the parent inventory fragment to notify of changes. - */ - public void setInventoryFragment(InventoryFragment fragment) { - this.inventoryFragment = fragment; - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -270,7 +261,7 @@ public class InventoryDetailFragment extends Fragment { setButtonsEnabled(true); if (response.isSuccessful()) { Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); - notifyParentAndGoBack(); + navigateBack(); } else { ErrorUtils.showErrorMessage(getContext(), response, "Update failed"); } @@ -289,7 +280,7 @@ public class InventoryDetailFragment extends Fragment { setButtonsEnabled(true); if (response.isSuccessful()) { Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); - notifyParentAndGoBack(); + navigateBack(); } else { ErrorUtils.showErrorMessage(getContext(), response, "Create failed"); } @@ -327,7 +318,7 @@ public class InventoryDetailFragment extends Fragment { setButtonsEnabled(true); if (response.isSuccessful()) { Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); - notifyParentAndGoBack(); + navigateBack(); } else { ErrorUtils.showErrorMessage(getContext(), response, "Delete failed"); } @@ -341,15 +332,6 @@ public class InventoryDetailFragment extends Fragment { }); } - /** - * Notifies the parent fragment of a change and navigates back. - */ - private void notifyParentAndGoBack() { - if (inventoryFragment != null) - inventoryFragment.onInventoryChanged(); - navigateBack(); - } - /** * Navigates back to the previous fragment. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 0842f6bc..577211bf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -18,7 +18,6 @@ import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.fragments.listfragments.ServiceFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; @@ -41,17 +40,9 @@ public class ServiceDetailFragment extends Fragment { private Button btnSaveService, btnDeleteService, btnBack; private int serviceId; private boolean isEditing = false; - private ServiceFragment serviceFragment; @Inject ServiceApi serviceApi; - /** - * Sets the parent service fragment to notify of changes. - */ - public void setServiceFragment(ServiceFragment fragment) { - this.serviceFragment = fragment; - } - @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -207,7 +198,7 @@ public class ServiceDetailFragment extends Fragment { } /** - * Set UI components from the layout. + * Initializes UI components from the layout. */ private void initViews(View view) { tvMode = view.findViewById(R.id.tvMode); @@ -220,4 +211,4 @@ public class ServiceDetailFragment extends Fragment { btnDeleteService = view.findViewById(R.id.btnDeleteService); btnBack = view.findViewById(R.id.btnBack); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java index 8982dd51..8b0d5fcc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.PageResponse; @@ -17,7 +16,6 @@ import okhttp3.MultipartBody; @HiltViewModel public class PetViewModel extends ViewModel { private final PetRepository repository; - private final MutableLiveData _petId = new MutableLiveData<>(); @Inject public PetViewModel(PetRepository repository) { From 6d990fbc6392174395f3cf728f8837f30802a1f9 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:15:36 -0600 Subject: [PATCH 061/137] Created Spinner Helper class and removed reducdent code --- .../AdoptionDetailFragment.java | 47 ++------ .../AppointmentDetailFragment.java | 104 +++--------------- .../InventoryDetailFragment.java | 21 +--- .../detailfragments/PetDetailFragment.java | 16 +-- .../ProductDetailFragment.java | 49 +++------ .../ProductSupplierDetailFragment.java | 60 +++------- .../detailfragments/RefundDetailFragment.java | 14 +-- .../petstoremobile/utils/SpinnerUtils.java | 68 ++++++++++++ 8 files changed, 134 insertions(+), 245 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index a135e36e..d9d84fd6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -15,6 +15,7 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.SpinnerUtils; import java.util.*; @@ -120,7 +121,9 @@ public class AdoptionDetailFragment extends Fragment { Response> r) { if (r.isSuccessful() && r.body() != null) { petList = r.body().getContent(); - populatePetSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); } } public void onFailure(Call> c, Throwable t) { @@ -129,24 +132,6 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Populates the pet selection spinner. - */ - private void populatePetSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Pet --"); - for (PetDTO p : petList) names.add(p.getPetName()); - spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedPetId != -1) { - for (int i = 0; i < petList.size(); i++) { - if (petList.get(i).getPetId().equals(preselectedPetId)) { - spinnerPet.setSelection(i + 1); break; - } - } - } - } - /** * Loads the list of customers from the API. */ @@ -157,7 +142,10 @@ public class AdoptionDetailFragment extends Fragment { Response> r) { if (r.isSuccessful() && r.body() != null) { customerList = r.body().getContent(); - populateCustomerSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); } } public void onFailure(Call> c, Throwable t) { @@ -166,25 +154,6 @@ public class AdoptionDetailFragment extends Fragment { }); } - /** - * Populates the customer selection spinner. - */ - private void populateCustomerSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Customer --"); - for (CustomerDTO c : customerList) - names.add(c.getFirstName() + " " + c.getLastName()); - spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedCustomerId != -1) { - for (int i = 0; i < customerList.size(); i++) { - if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) { - spinnerCustomer.setSelection(i + 1); break; - } - } - } - } - /** * Handles arguments to determine if the fragment is in edit or add mode. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 0f192dce..e1af3531 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -15,6 +15,7 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.SpinnerUtils; import java.util.*; @@ -96,16 +97,13 @@ public class AppointmentDetailFragment extends Fragment { * Configures the adapters for spinners. */ private void setupSpinners() { - spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, STATUSES)); + SpinnerUtils.setupStringSpinner(requireContext(), spinnerStatus, STATUSES); String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = String.format("%02d:00", HOURS[i]); - spinnerHour.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, hours)); - spinnerMinute.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, new String[]{"00","15","30","45"})); + SpinnerUtils.setupStringSpinner(requireContext(), spinnerHour, hours); + SpinnerUtils.setupStringSpinner(requireContext(), spinnerMinute, new String[]{"00","15","30","45"}); } /** @@ -144,7 +142,9 @@ public class AppointmentDetailFragment extends Fragment { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { petList = r.body().getContent(); - populatePetSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); } } public void onFailure(Call> c, Throwable t) { @@ -153,24 +153,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Populates the pet selection spinner. - */ - private void populatePetSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Pet --"); - for (PetDTO p : petList) names.add(p.getPetName()); - spinnerPet.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedPetId != -1) { - for (int i = 0; i < petList.size(); i++) { - if (petList.get(i).getPetId().equals(preselectedPetId)) { - spinnerPet.setSelection(i + 1); break; - } - } - } - } - /** * Loads the list of services from the API. */ @@ -180,7 +162,9 @@ public class AppointmentDetailFragment extends Fragment { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { serviceList = r.body().getContent(); - populateServiceSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerService, serviceList, + ServiceDTO::getServiceName, "-- Select Service --", + preselectedServiceId, ServiceDTO::getServiceId); } } public void onFailure(Call> c, Throwable t) { @@ -189,24 +173,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Populates the service selection spinner. - */ - private void populateServiceSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Service --"); - for (ServiceDTO s : serviceList) names.add(s.getServiceName()); - spinnerService.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedServiceId != -1) { - for (int i = 0; i < serviceList.size(); i++) { - if (serviceList.get(i).getServiceId().equals(preselectedServiceId)) { - spinnerService.setSelection(i + 1); break; - } - } - } - } - /** * Loads the list of customers from the API. */ @@ -216,7 +182,10 @@ public class AppointmentDetailFragment extends Fragment { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { customerList = r.body().getContent(); - populateCustomerSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); } } public void onFailure(Call> c, Throwable t) { @@ -225,25 +194,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Populates the customer spinner. - */ - private void populateCustomerSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Customer --"); - for (CustomerDTO c : customerList) - names.add(c.getFirstName() + " " + c.getLastName()); - spinnerCustomer.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedCustomerId != -1) { - for (int i = 0; i < customerList.size(); i++) { - if (customerList.get(i).getCustomerId().equals(preselectedCustomerId)) { - spinnerCustomer.setSelection(i + 1); break; - } - } - } - } - /** * Loads the list of stores from the API. */ @@ -253,7 +203,9 @@ public class AppointmentDetailFragment extends Fragment { public void onResponse(Call> c, Response> r) { if (r.isSuccessful() && r.body() != null) { storeList = r.body().getContent(); - populateStoreSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); } } public void onFailure(Call> c, Throwable t) { @@ -262,24 +214,6 @@ public class AppointmentDetailFragment extends Fragment { }); } - /** - * Populates the store spinner. - */ - private void populateStoreSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Store --"); - for (StoreDTO s : storeList) names.add(s.getStoreName()); - spinnerStore.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedStoreId != -1) { - for (int i = 0; i < storeList.size(); i++) { - if (storeList.get(i).getStoreId().equals(preselectedStoreId)) { - spinnerStore.setSelection(i + 1); break; - } - } - } - } - /** * Loads all appointments from the API. */ @@ -327,9 +261,7 @@ public class AppointmentDetailFragment extends Fragment { } // Pre-fill status - String status = a.getString("appointmentStatus", "Booked"); - for (int i = 0; i < STATUSES.length; i++) - if (STATUSES[i].equals(status)) { spinnerStatus.setSelection(i); break; } + SpinnerUtils.setSelectionByValue(spinnerStatus, a.getString("appointmentStatus", "Booked")); } else { tvMode.setText("Add Appointment"); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index a363e421..d174f240 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -27,6 +27,7 @@ import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.InputValidator; import java.util.ArrayList; import java.util.List; @@ -230,26 +231,12 @@ public class InventoryDetailFragment extends Fragment { return; } - String quantityStr = etQuantity.getText().toString().trim(); - if (quantityStr.isEmpty()) { - etQuantity.setError("Quantity is required"); - etQuantity.requestFocus(); + if (!InputValidator.isNotEmpty(etQuantity, "Quantity") || + !InputValidator.isPositiveInteger(etQuantity, "Quantity")) { return; } - int quantity; - try { - quantity = Integer.parseInt(quantityStr); - } catch (NumberFormatException e) { - etQuantity.setError("Invalid quantity"); - return; - } - - if (quantity < 0) { - etQuantity.setError("Quantity must be 0 or more"); - etQuantity.requestFocus(); - return; - } + int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); setButtonsEnabled(false); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 83ce6d51..d4add362 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -18,12 +18,12 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; import javax.inject.Inject; @@ -197,12 +197,9 @@ public class PetDetailFragment extends Fragment { etPetBreed.setText(getArguments().getString("petBreed")); etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); - String status = getArguments().getString("petStatus"); - if ("Available".equals(status)) { - spinnerPetStatus.setSelection(0); - } else { - spinnerPetStatus.setSelection(1); - } + + SpinnerUtils.setSelectionByValue(spinnerPetStatus, getArguments().getString("petStatus")); + btnDeletePet.setVisibility(View.VISIBLE); } else { // Pet is being added @@ -236,11 +233,8 @@ public class PetDetailFragment extends Fragment { * Initializes the spinner for pet status selection. */ private void setupSpinner() { - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, + SpinnerUtils.setupStringSpinner(requireContext(), spinnerPetStatus, new String[]{"Available", "Adopted"}); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerPetStatus.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 8baa9646..ae265b86 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -20,12 +20,15 @@ import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; import java.io.File; import java.math.BigDecimal; import java.util.*; import javax.inject.Inject; + import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @@ -130,29 +133,13 @@ public class ProductDetailFragment extends Fragment { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { categoryList = resource.data.getContent(); - populateCategorySpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerCategory, categoryList, + CategoryDTO::getCategoryName, "-- Select Category --", + preselectedCategoryId, CategoryDTO::getCategoryId); } }); } - /** - * Fills the spinner with category names. - */ - private void populateCategorySpinner() { - List names = new ArrayList<>(); - names.add("-- Select Category --"); - for (CategoryDTO c : categoryList) names.add(c.getCategoryName()); - spinnerCategory.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedCategoryId != -1) { - for (int i = 0; i < categoryList.size(); i++) { - if (categoryList.get(i).getCategoryId().equals(preselectedCategoryId)) { - spinnerCategory.setSelection(i + 1); break; - } - } - } - } - /** * Checks if the fragment was opened with existing product data for editing. */ @@ -253,28 +240,22 @@ public class ProductDetailFragment extends Fragment { * Validates input fields and saves product information to the backend. */ private void saveProduct() { - String name = etProductName.getText().toString().trim(); - String desc = etProductDesc.getText().toString().trim(); - String priceStr = etProductPrice.getText().toString().trim(); + if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return; - if (name.isEmpty()) { - etProductName.setError("Enter product name"); return; - } if (spinnerCategory.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return; } - if (priceStr.isEmpty()) { - etProductPrice.setError("Enter price"); return; + + if (!InputValidator.isNotEmpty(etProductPrice, "Price") || + !InputValidator.isPositiveDecimal(etProductPrice, "Price")) { + return; } + String name = etProductName.getText().toString().trim(); + String desc = etProductDesc.getText().toString().trim(); + BigDecimal price = new BigDecimal(etProductPrice.getText().toString().trim()); + CategoryDTO category = categoryList.get(spinnerCategory.getSelectedItemPosition() - 1); - BigDecimal price; - try { - price = new BigDecimal(priceStr); - } catch (Exception e) { - etProductPrice.setError("Invalid price"); return; - } - ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); if (isEditing) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index f1559957..3c6ce82b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -14,6 +14,8 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; import java.math.BigDecimal; import java.util.*; @@ -92,7 +94,9 @@ public class ProductSupplierDetailFragment extends Fragment { Response> r) { if (r.isSuccessful() && r.body() != null) { productList = r.body().getContent(); - populateProductSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); } } public void onFailure(Call> c, Throwable t) { @@ -101,24 +105,6 @@ public class ProductSupplierDetailFragment extends Fragment { }); } - /** - * Populates the product spinner. - */ - private void populateProductSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Product --"); - for (ProductDTO p : productList) names.add(p.getProdName()); - spinnerProduct.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedProductId != -1) { - for (int i = 0; i < productList.size(); i++) { - if (productList.get(i).getProdId().equals(preselectedProductId)) { - spinnerProduct.setSelection(i + 1); break; - } - } - } - } - /** * Loads the list of suppliers from the API. */ @@ -129,7 +115,9 @@ public class ProductSupplierDetailFragment extends Fragment { Response> r) { if (r.isSuccessful() && r.body() != null) { supplierList = r.body().getContent(); - populateSupplierSpinner(); + SpinnerUtils.populateSpinner(requireContext(), spinnerSupplier, supplierList, + SupplierDTO::getSupCompany, "-- Select Supplier --", + preselectedSupplierId, SupplierDTO::getSupId); } } public void onFailure(Call> c, Throwable t) { @@ -138,24 +126,6 @@ public class ProductSupplierDetailFragment extends Fragment { }); } - /** - * Populates the supplier spinner. - */ - private void populateSupplierSpinner() { - List names = new ArrayList<>(); - names.add("-- Select Supplier --"); - for (SupplierDTO s : supplierList) names.add(s.getSupCompany()); - spinnerSupplier.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, names)); - if (preselectedSupplierId != -1) { - for (int i = 0; i < supplierList.size(); i++) { - if (supplierList.get(i).getSupId().equals(preselectedSupplierId)) { - spinnerSupplier.setSelection(i + 1); break; - } - } - } - } - /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -186,19 +156,15 @@ public class ProductSupplierDetailFragment extends Fragment { if (spinnerSupplier.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; } - String costStr = etCost.getText().toString().trim(); - if (costStr.isEmpty()) { - etCost.setError("Enter cost"); return; + + if (!InputValidator.isNotEmpty(etCost, "Cost") || + !InputValidator.isPositiveDecimal(etCost, "Cost")) { + return; } ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1); - BigDecimal cost; - try { - cost = new BigDecimal(costStr); - } catch (Exception e) { - etCost.setError("Invalid cost"); return; - } + BigDecimal cost = new BigDecimal(etCost.getText().toString().trim()); ProductSupplierDTO dto = new ProductSupplierDTO( product.getProdId(), supplier.getSupId(), cost); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index ce782a04..c607c6de 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -19,6 +19,7 @@ import com.example.petstoremobile.api.SaleApi; import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.SpinnerUtils; import javax.inject.Inject; @@ -109,13 +110,7 @@ public class RefundDetailFragment extends Fragment { tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); // Pre-select payment method - String payment = getArguments().getString("paymentMethod"); - ArrayAdapter adapter = (ArrayAdapter) spinnerRefundPayment.getAdapter(); - if (adapter != null && payment != null) { - int pos = adapter.getPosition(payment); - if (pos >= 0) - spinnerRefundPayment.setSelection(pos); - } + SpinnerUtils.setSelectionByValue(spinnerRefundPayment, getArguments().getString("paymentMethod")); } } @@ -134,10 +129,7 @@ public class RefundDetailFragment extends Fragment { } private void setupSpinner() { - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, + SpinnerUtils.setupStringSpinner(requireContext(), spinnerRefundPayment, new String[] { "Cash", "Card", "Debit" }); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerRefundPayment.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java new file mode 100644 index 00000000..5095199e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.Spinner; + +import com.example.petstoremobile.adapters.BlackTextArrayAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Utility class for Spinners. + */ +public class SpinnerUtils { + + /** + * Populates a spinner with a list of items and handles pre-selection. + */ + public static void populateSpinner(Context context, Spinner spinner, List data, + Function nameExtractor, String defaultText, + Long preselectedId, Function idExtractor) { + List names = new ArrayList<>(); + if (defaultText != null) { + names.add(defaultText); + } + + for (T item : data) { + names.add(nameExtractor.apply(item)); + } + + spinner.setAdapter(new BlackTextArrayAdapter<>(context, + android.R.layout.simple_spinner_item, names)); + + if (preselectedId != null && preselectedId != -1) { + int offset = (defaultText != null) ? 1 : 0; + for (int i = 0; i < data.size(); i++) { + if (idExtractor.apply(data.get(i)).equals(preselectedId)) { + spinner.setSelection(i + offset); + break; + } + } + } + } + + /** + * Sets the selection of a spinner based on a string value. + */ + public static void setSelectionByValue(Spinner spinner, String value) { + if (value == null || spinner.getAdapter() == null) return; + ArrayAdapter adapter = (ArrayAdapter) spinner.getAdapter(); + int pos = adapter.getPosition(value); + if (pos >= 0) { + spinner.setSelection(pos); + } + } + + /** + * Configures a simple string array spinner. + */ + public static void setupStringSpinner(Context context, Spinner spinner, String[] items) { + BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(context, + android.R.layout.simple_spinner_item, items); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + } +} From c99d9d21f05070373a7ce96989d23c62ad3533d4 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:38:46 -0600 Subject: [PATCH 062/137] Created help class for displaying diolog and removed redundent code --- .../AdoptionDetailFragment.java | 34 +++++--------- .../AppointmentDetailFragment.java | 41 +++++----------- .../detailfragments/PetDetailFragment.java | 46 ++++++++---------- .../ProductDetailFragment.java | 21 ++++----- .../ProductSupplierDetailFragment.java | 28 +++++------ .../ServiceDetailFragment.java | 45 ++++++++---------- .../SupplierDetailFragment.java | 45 ++++++++---------- .../petstoremobile/utils/DialogUtils.java | 47 +++++++++++++++++++ 8 files changed, 150 insertions(+), 157 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index d9d84fd6..61bed9f7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -6,14 +6,13 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.SpinnerUtils; @@ -84,8 +83,7 @@ public class AdoptionDetailFragment extends Fragment { * Configures the spinner for adoption status. */ private void setupSpinners() { - spinnerStatus.setAdapter(new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, STATUSES)); + SpinnerUtils.setupStringSpinner(requireContext(), spinnerStatus, STATUSES); } /** @@ -172,12 +170,7 @@ public class AdoptionDetailFragment extends Fragment { btnDelete.setVisibility(View.VISIBLE); // Pre-fill status - String status = a.getString("adoptionStatus", "Pending"); - for (int i = 0; i < STATUSES.length; i++) { - if (STATUSES[i].equals(status)) { - spinnerStatus.setSelection(i); break; - } - } + SpinnerUtils.setSelectionByValue(spinnerStatus, a.getString("adoptionStatus", "Pending")); } else { tvMode.setText("Add Adoption"); btnDelete.setVisibility(View.GONE); @@ -247,18 +240,15 @@ public class AdoptionDetailFragment extends Fragment { * Shows a confirmation dialog before deleting an adoption request. */ private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Adoption?") - .setPositiveButton("Yes", (d, w) -> - adoptionApi.deleteAdoption(adoptionId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { navigateBack(); } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", - Toast.LENGTH_SHORT).show(); - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> + adoptionApi.deleteAdoption(adoptionId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { navigateBack(); } + public void onFailure(Call c, Throwable t) { + Toast.makeText(getContext(), "Delete failed", + Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index e1af3531..4f933788 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -6,14 +6,13 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.SpinnerUtils; @@ -317,7 +316,7 @@ public class AppointmentDetailFragment extends Fragment { 0 ); if (selected.before(Calendar.getInstance())) { - showErrorDialog("Invalid Time", + DialogUtils.showInfoDialog(requireContext(), "Invalid Time", "Booked appointments must be in the future. " + "Please select a future date and time."); return; @@ -368,7 +367,7 @@ public class AppointmentDetailFragment extends Fragment { // Show proper dialog based on error type if (errorBody.toLowerCase().contains("future")) { - showErrorDialog("Invalid Date/Time", + DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time", "Booked appointments must be scheduled in the future. " + "Please select a future date and time."); } else if (errorBody.toLowerCase().contains("not available") || @@ -379,7 +378,7 @@ public class AppointmentDetailFragment extends Fragment { } } catch (Exception e) { Log.e("APPT_SAVE", "Failed to read error body"); - showErrorDialog("Error", "Something went wrong. Please try again."); + DialogUtils.showInfoDialog(requireContext(), "Error", "Something went wrong. Please try again."); } } } @@ -395,7 +394,7 @@ public class AppointmentDetailFragment extends Fragment { * Shows a specialized dialog when a time slot is not available. */ private void showNoAvailabilityDialog() { - new AlertDialog.Builder(requireContext()) + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") .setMessage("This time slot is already booked for the selected service and store. Please choose a different time or date.") .setPositiveButton("Change Time", (d, w) -> d.dismiss()) @@ -404,32 +403,18 @@ public class AppointmentDetailFragment extends Fragment { .show(); } - /** - * Shows a generic error dialog with a title and message. - */ - private void showErrorDialog(String title, String message) { - new AlertDialog.Builder(requireContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton("OK", null) - .show(); - } - /** * Shows a confirmation dialog and handles the deletion of an appointment. */ private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Appointment?") - .setPositiveButton("Yes", (d, w) -> - appointmentApi.deleteAppointment(appointmentId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { navigateBack(); } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> + appointmentApi.deleteAppointment(appointmentId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { navigateBack(); } + public void onFailure(Call c, Throwable t) { + Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index d4add362..78d5efb7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -3,7 +3,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -21,6 +20,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.SpinnerUtils; @@ -144,34 +144,26 @@ public class PetDetailFragment extends Fragment { * Displays a confirmation dialog and handles the deletion of a pet. */ private void deletePet() { - //Alert the user to confirm the delete - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Pet") - .setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?") - .setPositiveButton("Delete", (dialog, which) -> { - //if they say yes then delete the pet - petApi.deletePet((long) petId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); - Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet"); - } + DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> + petApi.deletePet((long) petId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); + Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet"); } + } - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t)); - Log.e("PetDetailFragment", "Error deleting pet", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); - }) - .setNegativeButton("Cancel", null) - .show(); + @Override + public void onFailure(Call call, Throwable t) { + ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t)); + Log.e("PetDetailFragment", "Error deleting pet", t); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index ae265b86..64f2c7f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -11,11 +11,11 @@ import androidx.navigation.fragment.NavHostFragment; import com.bumptech.glide.Glide; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; @@ -286,17 +286,14 @@ public class ProductDetailFragment extends Fragment { * Displays a confirmation dialog before deleting the product. */ private void confirmDelete() { - new androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle("Delete Product?") - .setPositiveButton("Yes", (d, w) -> - viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { - navigateBack(); - } else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> + viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + navigateBack(); + } else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 3c6ce82b..3db78dd0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -5,14 +5,13 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.SpinnerUtils; @@ -201,20 +200,17 @@ public class ProductSupplierDetailFragment extends Fragment { * Shows a confirmation dialog before deleting a product-supplier relationship. */ private void confirmDelete() { - new AlertDialog.Builder(requireContext()) - .setTitle("Delete?") - .setPositiveButton("Yes", (d, w) -> - productSupplierApi.deleteProductSupplier(editProductId, editSupplierId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - navigateBack(); - } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", - Toast.LENGTH_SHORT).show(); - } - })) - .setNegativeButton("No", null).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> + productSupplierApi.deleteProductSupplier(editProductId, editSupplierId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + navigateBack(); + } + public void onFailure(Call c, Throwable t) { + Toast.makeText(getContext(), "Delete failed", + Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 577211bf..a70cb266 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -19,6 +18,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; @@ -134,33 +134,26 @@ public class ServiceDetailFragment extends Fragment { * Displays a confirmation dialog and handles the deletion of a service. */ private void deleteService() { - //Alert the user to confirm the delete - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Service") - .setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?") - .setPositiveButton("Delete", (dialog, which) -> { - serviceApi.deleteService((long) serviceId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); - Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service"); - } + DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> + serviceApi.deleteService((long) serviceId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); + Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service"); } + } - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t)); - Log.e("ServiceDetailFragment", "Error deleting service", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); - }) - .setNegativeButton("Cancel", null) - .show(); + @Override + public void onFailure(Call call, Throwable t) { + ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t)); + Log.e("ServiceDetailFragment", "Error deleting service", t); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index e4b41598..3001faad 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -19,6 +18,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.UIUtils; @@ -138,33 +138,26 @@ public class SupplierDetailFragment extends Fragment { * Displays a confirmation dialog and handles the deletion of a supplier. */ private void deleteSupplier() { - //Alert the user to confirm the delete - new AlertDialog.Builder(requireContext()) - .setTitle("Delete Supplier") - .setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?") - .setPositiveButton("Delete", (dialog, which) -> { - supplierApi.deleteSupplier((long) supId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); - Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier"); - } + DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> + supplierApi.deleteSupplier((long) supId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); + Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier"); } + } - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t)); - Log.e("SupplierDetailFragment", "Error deleting supplier", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); - }) - .setNegativeButton("Cancel", null) - .show(); + @Override + public void onFailure(Call call, Throwable t) { + ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t)); + Log.e("SupplierDetailFragment", "Error deleting supplier", t); + Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java new file mode 100644 index 00000000..55436846 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java @@ -0,0 +1,47 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import androidx.appcompat.app.AlertDialog; + +/** + * Utility class for creating and displaying common dialogs. + */ +public class DialogUtils { + + /** + * Interface for handling dialog button clicks. + */ + public interface DialogCallback { + void onConfirm(); + } + + /** + * Shows a confirmation dialog with "Yes" and "No" buttons. + */ + public static void showConfirmDialog(Context context, String title, String message, DialogCallback callback) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton("Yes", (dialog, which) -> callback.onConfirm()) + .setNegativeButton("No", null) + .show(); + } + + /** + * Shows a delete confirmation dialog. + */ + public static void showDeleteConfirmDialog(Context context, String itemName, DialogCallback callback) { + showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback); + } + + /** + * Shows a simple information or error dialog with an "OK" button. + */ + public static void showInfoDialog(Context context, String title, String message) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton("OK", null) + .show(); + } +} From 3555b3d2a120e06177df13314bb46de842dd0f79 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:27:32 -0600 Subject: [PATCH 063/137] Refactored more of the project to MVVM and created helper class RetrofitUtil to reduce redundent code --- .../activities/HomeActivity.java | 11 +- .../fragments/ChatFragment.java | 146 ++++------- .../fragments/ProfileFragment.java | 129 +++------- .../AdoptionDetailFragment.java | 133 +++++------ .../AppointmentDetailFragment.java | 226 +++++++----------- .../InventoryDetailFragment.java | 129 ++++------ .../detailfragments/PetDetailFragment.java | 94 +++----- .../ProductSupplierDetailFragment.java | 132 +++++----- .../ServiceDetailFragment.java | 95 +++----- .../SupplierDetailFragment.java | 96 +++----- .../PetProfileFragment.java | 50 +--- .../repositories/AdoptionRepository.java | 91 +------ .../repositories/AppointmentRepository.java | 91 +------ .../repositories/AuthRepository.java | 105 ++------ .../repositories/CategoryRepository.java | 23 +- .../repositories/CustomerRepository.java | 50 ++++ .../repositories/InventoryRepository.java | 116 ++------- .../repositories/PetRepository.java | 129 ++-------- .../repositories/ProductRepository.java | 124 ++-------- .../ProductSupplierRepository.java | 74 +----- .../repositories/PurchaseOrderRepository.java | 38 +-- .../repositories/ServiceRepository.java | 91 +------ .../repositories/StoreRepository.java | 37 +++ .../repositories/SupplierRepository.java | 99 ++------ .../services/ChatNotificationService.java | 74 ++---- .../petstoremobile/utils/RetrofitUtils.java | 74 ++++++ .../viewmodels/CustomerViewModel.java | 37 +++ .../viewmodels/StoreViewModel.java | 30 +++ 28 files changed, 855 insertions(+), 1669 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 947b98e1..d9e41f96 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -80,6 +80,7 @@ public class HomeActivity extends AppCompatActivity { @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); + setIntent(intent); // Set the new intent so fragments can access updated extras handleIntent(intent); } @@ -88,13 +89,9 @@ public class HomeActivity extends AppCompatActivity { */ private void handleIntent(Intent intent) { if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { - Bundle args = new Bundle(); - if (intent.hasExtra("conversation_id")) { - args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1)); - } - // Use NavController to navigate - if (navController != null) { - navController.navigate(R.id.nav_chat, args); + if (bottomNav != null) { + // Navigate by selecting the bottom nav item. + bottomNav.setSelectedItemId(R.id.nav_chat); } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 8ceed48f..25f4a7f1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -34,6 +34,7 @@ import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; +import com.example.petstoremobile.utils.RetrofitUtils; import com.example.petstoremobile.websocket.StompChatManager; import java.util.*; @@ -202,6 +203,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (getArguments() != null && getArguments().containsKey("conversation_id")) { activeConversationId = getArguments().getLong("conversation_id"); + } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { + activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); + getActivity().getIntent().removeExtra("conversation_id"); + getActivity().getIntent().removeExtra("navigate_to"); } loadCustomers(); @@ -211,74 +216,51 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis * Fetches a list of customers from the API to display customer names for the chat list. */ private void loadCustomers() { - customerApi.getAllCustomers(0, 100).enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - for (CustomerDTO c : response.body().getContent()) { - customerNames.put(c.getCustomerId(), c.getFullName()); - } - } - loadConversations(); + customerApi.getAllCustomers(0, 100).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + for (CustomerDTO c : result.getContent()) { + customerNames.put(c.getCustomerId(), c.getFullName()); } - - @Override - public void onFailure(@NonNull Call> call, - @NonNull Throwable t) { - loadConversations(); - } - }); + loadConversations(); + })); } /** * Retrieves all conversations for the current user and populates the chat drawer. */ private void loadConversations() { - chatApi.getAllConversations().enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - chatList.clear(); - List loaded = response.body().stream() - .map(dto -> { - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - return new Chat(String.valueOf(dto.getId()), - name, dto.getLastMessage(), - dto.getCustomerId(), dto.getStaffId()); - }) - .collect(Collectors.toList()); - chatList.addAll(loaded); - chatAdapter.notifyDataSetChanged(); - - if (activeConversationId != null) { - setConversationActive(true); - // Update title to customer name of active conversation - for (Chat chat : chatList) { - if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - tvChatTitle.setText(chat.getCustomerName()); - break; - } - } - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - loadMessageHistory(activeConversationId); - } else { - messageList.clear(); - messageAdapter.notifyDataSetChanged(); - setConversationActive(false); + chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + chatList.clear(); + List loaded = result.stream() + .map(dto -> { + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + return new Chat(String.valueOf(dto.getId()), + name, dto.getLastMessage(), + dto.getCustomerId(), dto.getStaffId()); + }) + .collect(Collectors.toList()); + chatList.addAll(loaded); + chatAdapter.notifyDataSetChanged(); + + if (activeConversationId != null) { + setConversationActive(true); + // Update title to customer name of active conversation + for (Chat chat : chatList) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + tvChatTitle.setText(chat.getCustomerName()); + break; } } + if (stompChatManager != null) { + stompChatManager.subscribeToConversation(activeConversationId); + } + loadMessageHistory(activeConversationId); + } else { + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + setConversationActive(false); } - @Override - public void onFailure(@NonNull Call> call, - @NonNull Throwable t) { - Log.e(TAG, "Error loading conversations", t); - } - }); + })); } /** @@ -302,25 +284,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis * Fetches the full message history for a specific conversation from the API. */ private void loadMessageHistory(Long conversationId) { - messageApi.getMessages(conversationId).enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - messageList.clear(); - for (MessageDTO dto : response.body()) { - messageList.add(dtoToModel(dto)); - } - messageAdapter.notifyDataSetChanged(); - scrollToBottom(); - } + messageApi.getMessages(conversationId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + messageList.clear(); + for (MessageDTO dto : result) { + messageList.add(dtoToModel(dto)); } - @Override - public void onFailure(@NonNull Call> call, - @NonNull Throwable t) { - Log.e(TAG, "Error loading messages", t); - } - }); + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + })); } /** @@ -339,23 +310,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //calls api to send the message messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, - @NonNull Response response) { - if (response.isSuccessful() && response.body() != null) { - messageList.add(dtoToModel(response.body())); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); - } - } - @Override - public void onFailure(@NonNull Call call, - @NonNull Throwable t) { - Log.e(TAG, "Send failed", t); - } - }); + .enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + messageList.add(dtoToModel(result)); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + loadConversations(); + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index b4f15e69..bcc8a0d8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -27,6 +27,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.RetrofitUtils; import com.example.petstoremobile.utils.UIUtils; import java.io.File; @@ -189,47 +190,31 @@ public class ProfileFragment extends Fragment { * Fetches current user profile data from the API and then updates the UI. */ private void loadProfileData() { - authApi.getMe().enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - //if the response is successful and the body is not null then set the user to the view - if (response.isSuccessful() && response.body() != null) { - currentUser = response.body(); + authApi.getMe().enqueue(RetrofitUtils.createCallback(requireContext(), "PROFILE", null, result -> { + currentUser = result; - //set the user data to the view - tvProfileName.setText(currentUser.getFullName()); - tvProfileEmail.setText(currentUser.getEmail()); - tvProfilePhone.setText(currentUser.getPhone()); - tvProfileRole.setText(currentUser.getRole()); + //set the user data to the view + tvProfileName.setText(currentUser.getFullName()); + tvProfileEmail.setText(currentUser.getEmail()); + tvProfilePhone.setText(currentUser.getPhone()); + tvProfileRole.setText(currentUser.getRole()); - // get the avatar endpoint to load profile image and the token for authorization - String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; - String token = tokenManager.getToken(); + // get the avatar endpoint to load profile image and the token for authorization + String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; + String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { - @Override - public void onResourceReady() { - hasImage = true; - } - - @Override - public void onLoadFailed() { - hasImage = false; - } - }); + GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; } - else { - Log.e("onResponse: ", response.message()); - ErrorUtils.showErrorMessage(getContext(), response, "Failed to load profile"); - } - } - @Override - public void onFailure(Call call, Throwable t) { - Log.e("PROFILE", "onFailure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show(); - } - }); + @Override + public void onLoadFailed() { + hasImage = false; + } + }); + })); } /** @@ -245,25 +230,11 @@ public class ProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); //Call the backend to upload the avatar - authApi.uploadAvatar(body).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - currentUser = response.body(); - Toast.makeText(requireContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); - // Reload image after successful upload - loadProfileData(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to upload avatar"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("UPLOAD_AVATAR", "Failure: " + t.getMessage()); - Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + authApi.uploadAvatar(body).enqueue(RetrofitUtils.createCallback(requireContext(), "UPLOAD_AVATAR", "Avatar updated successfully", result -> { + currentUser = result; + // Reload image after successful upload + loadProfileData(); + })); } catch (Exception e) { Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage()); } @@ -273,24 +244,10 @@ public class ProfileFragment extends Fragment { * Sends a request to the API to delete the current user's avatar image. */ private void deleteAvatar() { - authApi.deleteAvatar().enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(requireContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); - hasImage = false; - imgProfile.setImageResource(R.drawable.placeholder); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to remove avatar"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("DELETE_AVATAR", "Failure: " + t.getMessage()); - Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + authApi.deleteAvatar().enqueue(RetrofitUtils.createCallback(requireContext(), "DELETE_AVATAR", "Avatar removed successfully", result -> { + hasImage = false; + imgProfile.setImageResource(R.drawable.placeholder); + })); } /** @@ -300,25 +257,11 @@ public class ProfileFragment extends Fragment { Map updates = new HashMap<>(); updates.put(fieldName, value); - authApi.updateMe(updates).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - currentUser = response.body(); - // Update the view with the new data from backend - tvProfileEmail.setText(currentUser.getEmail()); - tvProfilePhone.setText(currentUser.getPhone()); - Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to update profile"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("UPDATE_PROFILE", "Failure: " + t.getMessage()); - Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + authApi.updateMe(updates).enqueue(RetrofitUtils.createCallback(requireContext(), "UPDATE_PROFILE", "Profile updated successfully", result -> { + currentUser = result; + // Update the view with the new data from backend + tvProfileEmail.setText(currentUser.getEmail()); + tvProfilePhone.setText(currentUser.getPhone()); + })); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 61bed9f7..2aa140bf 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -7,21 +7,21 @@ import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.viewmodels.AdoptionViewModel; +import com.example.petstoremobile.viewmodels.CustomerViewModel; +import com.example.petstoremobile.viewmodels.PetViewModel; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; /** * Fragment for displaying and editing adoption request details. @@ -44,9 +44,17 @@ public class AdoptionDetailFragment extends Fragment { private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; - @Inject AdoptionApi adoptionApi; - @Inject PetApi petApi; - @Inject CustomerApi customerApi; + private AdoptionViewModel adoptionViewModel; + private PetViewModel petViewModel; + private CustomerViewModel customerViewModel; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); + petViewModel = new ViewModelProvider(this).get(PetViewModel.class); + customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -113,43 +121,29 @@ public class AdoptionDetailFragment extends Fragment { * Loads the list of pets from the API. */ private void loadPets() { - petApi.getAllPets(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - petList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("ADOPTION", "Pet load failed: " + t.getMessage()); - } - }); + petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + petList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + }); } /** * Loads the list of customers from the API. */ private void loadCustomers() { - customerApi.getAllCustomers(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - customerList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("ADOPTION", "Customer load failed: " + t.getMessage()); - } - }); + customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + customerList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); + } + }); } /** @@ -204,51 +198,40 @@ public class AdoptionDetailFragment extends Fragment { status ); - Log.d("ADOPTION_SAVE", "petId=" + pet.getPetId() - + " customerId=" + customer.getCustomerId() - + " date=" + date + " status=" + status); - if (isEditing) { - adoptionApi.updateAdoption(adoptionId, dto).enqueue(simpleCallback("Updated")); - } else { - adoptionApi.createAdoption(dto).enqueue(simpleCallback("Saved")); - } - } - - /** - * callback for adoption save/update operations. - */ - private Callback simpleCallback(String msg) { - return new Callback<>() { - public void onResponse(Call c, Response r) { - Log.d("ADOPTION_SAVE", "Response: " + r.code()); - if (r.isSuccessful()) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + adoptionViewModel.updateAdoption(adoptionId, dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - } - public void onFailure(Call c, Throwable t) { - Log.e("ADOPTION_SAVE", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }; + }); + } else { + adoptionViewModel.createAdoption(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } /** * Shows a confirmation dialog before deleting an adoption request. */ private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> - adoptionApi.deleteAdoption(adoptionId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { navigateBack(); } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", - Toast.LENGTH_SHORT).show(); - } - })); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Adoption", () -> + adoptionViewModel.deleteAdoption(adoptionId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 4f933788..f137bdfd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -7,21 +7,23 @@ import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.viewmodels.AppointmentViewModel; +import com.example.petstoremobile.viewmodels.CustomerViewModel; +import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.ServiceViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; /** * Fragment for displaying and editing appointment details. @@ -46,17 +48,26 @@ public class AppointmentDetailFragment extends Fragment { private List serviceList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); - private List allAppointments = new ArrayList<>(); private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; private final String[] STATUSES = {"Booked","Completed","Cancelled"}; - @Inject AppointmentApi appointmentApi; - @Inject PetApi petApi; - @Inject ServiceApi serviceApi; - @Inject CustomerApi customerApi; - @Inject StoreApi storeApi; + private AppointmentViewModel appointmentViewModel; + private PetViewModel petViewModel; + private ServiceViewModel serviceViewModel; + private StoreViewModel storeViewModel; + private CustomerViewModel customerViewModel; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); + petViewModel = new ViewModelProvider(this).get(PetViewModel.class); + serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -129,102 +140,63 @@ public class AppointmentDetailFragment extends Fragment { loadServices(); loadCustomers(); loadStores(); - loadAllAppointments(); } /** - * Loads the list of pets from the API. + * Loads the list of pets from the ViewModel. */ private void loadPets() { - petApi.getAllPets(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, Response> r) { - if (r.isSuccessful() && r.body() != null) { - petList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("APPT", "Pet load failed: " + t.getMessage()); - } - }); + petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + petList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + }); } /** * Loads the list of services from the API. */ private void loadServices() { - serviceApi.getAllServices(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, Response> r) { - if (r.isSuccessful() && r.body() != null) { - serviceList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerService, serviceList, - ServiceDTO::getServiceName, "-- Select Service --", - preselectedServiceId, ServiceDTO::getServiceId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("APPT", "Service load failed: " + t.getMessage()); - } - }); + serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + serviceList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerService, serviceList, + ServiceDTO::getServiceName, "-- Select Service --", + preselectedServiceId, ServiceDTO::getServiceId); + } + }); } /** * Loads the list of customers from the API. */ private void loadCustomers() { - customerApi.getAllCustomers(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, Response> r) { - if (r.isSuccessful() && r.body() != null) { - customerList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("APPT", "Customer load failed: " + t.getMessage()); - } - }); + customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + customerList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); + } + }); } /** * Loads the list of stores from the API. */ private void loadStores() { - storeApi.getAllStores(0, 50) - .enqueue(new Callback>() { - public void onResponse(Call> c, Response> r) { - if (r.isSuccessful() && r.body() != null) { - storeList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("APPT", "Store load failed: " + t.getMessage()); - } - }); - } - - /** - * Loads all appointments from the API. - */ - private void loadAllAppointments() { - appointmentApi.getAllAppointments(0, 500) - .enqueue(new Callback>() { - public void onResponse(Call> c, Response> r) { - if (r.isSuccessful() && r.body() != null) - allAppointments = r.body().getContent(); - } - public void onFailure(Call> c, Throwable t) {} - }); + storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + }); } /** @@ -337,57 +309,36 @@ public class AppointmentDetailFragment extends Fragment { Collections.singletonList(pet.getPetId()) ); - Log.d("APPT_SAVE", "customerId=" + customer.getCustomerId() - + " storeId=" + store.getStoreId() - + " serviceId=" + service.getServiceId() - + " petId=" + pet.getPetId() - + " date=" + date + " time=" + time); + androidx.lifecycle.Observer> observer = resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), isEditing ? "Updated" : "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + handleSaveError(resource.message); + } + }; if (isEditing) { - appointmentApi.updateAppointment(appointmentId, dto).enqueue(simpleCallback("Updated")); + appointmentViewModel.updateAppointment(appointmentId, dto).observe(getViewLifecycleOwner(), observer); } else { - appointmentApi.createAppointment(dto).enqueue(simpleCallback("Saved")); + appointmentViewModel.createAppointment(dto).observe(getViewLifecycleOwner(), observer); } } - /** - * callback for appointment save/update operations. - */ - private Callback simpleCallback(String msg) { - return new Callback<>() { - public void onResponse(Call c, Response r) { - Log.d("APPT_SAVE", "Response: " + r.code()); - if (r.isSuccessful()) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - try { - String errorBody = r.errorBody().string(); - Log.e("APPT_SAVE", "Error: " + errorBody); - - // Show proper dialog based on error type - if (errorBody.toLowerCase().contains("future")) { - DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time", - "Booked appointments must be scheduled in the future. " + - "Please select a future date and time."); - } else if (errorBody.toLowerCase().contains("not available") || - errorBody.toLowerCase().contains("time is not available")) { - showNoAvailabilityDialog(); - } else { - ErrorUtils.showErrorMessage(getContext(), r, "Something went wrong. Please try again."); - } - } catch (Exception e) { - Log.e("APPT_SAVE", "Failed to read error body"); - DialogUtils.showInfoDialog(requireContext(), "Error", "Something went wrong. Please try again."); - } - } + private void handleSaveError(String errorMessage) { + if (errorMessage != null) { + Log.e("APPT_SAVE", "Error: " + errorMessage); + if (errorMessage.toLowerCase().contains("future")) { + DialogUtils.showInfoDialog(requireContext(), "Invalid Date/Time", + "Booked appointments must be scheduled in the future."); + } else if (errorMessage.toLowerCase().contains("not available")) { + showNoAvailabilityDialog(); + } else { + Toast.makeText(getContext(), "Operation failed", Toast.LENGTH_SHORT).show(); } - - public void onFailure(Call c, Throwable t) { - Log.e("APPT_SAVE", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }; + } else { + Toast.makeText(getContext(), "Something went wrong", Toast.LENGTH_SHORT).show(); + } } /** @@ -396,7 +347,7 @@ public class AppointmentDetailFragment extends Fragment { private void showNoAvailabilityDialog() { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle("No Availability") - .setMessage("This time slot is already booked for the selected service and store. Please choose a different time or date.") + .setMessage("This time slot is already booked. Please choose a different time or date.") .setPositiveButton("Change Time", (d, w) -> d.dismiss()) .setNegativeButton("Cancel Booking", (d, w) -> navigateBack()) .setCancelable(false) @@ -407,14 +358,15 @@ public class AppointmentDetailFragment extends Fragment { * Shows a confirmation dialog and handles the deletion of an appointment. */ private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> - appointmentApi.deleteAppointment(appointmentId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { navigateBack(); } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); - } - })); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Appointment", () -> + appointmentViewModel.deleteAppointment(appointmentId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed", Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index d174f240..e2b0984d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -14,30 +14,26 @@ import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; -import com.example.petstoremobile.api.InventoryApi; -import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.InventoryViewModel; +import com.example.petstoremobile.viewmodels.ProductViewModel; import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; /** * Fragment for displaying and editing inventory item details. @@ -50,8 +46,8 @@ public class InventoryDetailFragment extends Fragment { private android.widget.EditText etQuantity; private Button btnSave, btnDelete, btnBack; - @Inject InventoryApi inventoryApi; - @Inject ProductApi productApi; + private InventoryViewModel inventoryViewModel; + private ProductViewModel productViewModel; private boolean isEditing = false; private long inventoryId = -1; @@ -68,7 +64,14 @@ public class InventoryDetailFragment extends Fragment { private ArrayAdapter dropdownAdapter; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); + productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); @@ -150,30 +153,21 @@ public class InventoryDetailFragment extends Fragment { * Searches for products matching the query from the backend. */ private void searchProducts(String query) { - productApi.getAllProducts(query, 0, 20).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, - Response> response) { - if (response.isSuccessful() && response.body() != null) { - productSuggestions.clear(); - productSuggestions.addAll(response.body().getContent()); + productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productSuggestions.clear(); + productSuggestions.addAll(resource.data.getContent()); - // Build display strings: "Product Name (ID: X)" - List names = new ArrayList<>(); - for (ProductDTO p : productSuggestions) { - names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); - } - - dropdownAdapter.clear(); - dropdownAdapter.addAll(names); - dropdownAdapter.notifyDataSetChanged(); - etProductSearch.showDropDown(); + // Build display strings: "Product Name (ID: X)" + List names = new ArrayList<>(); + for (ProductDTO p : productSuggestions) { + names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); } - } - @Override - public void onFailure(Call> call, Throwable t) { - Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); + dropdownAdapter.clear(); + dropdownAdapter.addAll(names); + dropdownAdapter.notifyDataSetChanged(); + etProductSearch.showDropDown(); } }); } @@ -242,41 +236,23 @@ public class InventoryDetailFragment extends Fragment { setButtonsEnabled(false); if (isEditing) { - inventoryApi.updateInventory(inventoryId, request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - setButtonsEnabled(true); - if (response.isSuccessful()) { - Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Update failed"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - setButtonsEnabled(true); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + inventoryViewModel.updateInventory(inventoryId, request).observe(getViewLifecycleOwner(), resource -> { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Inventory updated", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } else { - inventoryApi.createInventory(request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - setButtonsEnabled(true); - if (response.isSuccessful()) { - Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Create failed"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - setButtonsEnabled(true); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + inventoryViewModel.createInventory(request).observe(getViewLifecycleOwner(), resource -> { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Inventory created", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } @@ -299,22 +275,13 @@ public class InventoryDetailFragment extends Fragment { */ private void deleteInventory() { setButtonsEnabled(false); - inventoryApi.deleteInventory(inventoryId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - setButtonsEnabled(true); - if (response.isSuccessful()) { - Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Delete failed"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - setButtonsEnabled(true); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + inventoryViewModel.deleteInventory(inventoryId).observe(getViewLifecycleOwner(), resource -> { + setButtonsEnabled(true); + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Inventory deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 78d5efb7..afff4dd1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -4,9 +4,9 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,20 +17,15 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; - -import javax.inject.Inject; +import com.example.petstoremobile.viewmodels.PetViewModel; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; /** * Fragment for displaying and editing pet details. @@ -45,7 +40,13 @@ public class PetDetailFragment extends Fragment { private int petId; private boolean isEditing = false; - @Inject PetApi petApi; + private PetViewModel viewModel; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PetViewModel.class); + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -97,44 +98,24 @@ public class PetDetailFragment extends Fragment { if (isEditing) { // Update existing pet petDTO.setPetId((long) petId); - petApi.updatePet((long) petId, petDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); - Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to update pet"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t)); - Log.e("PetDetailFragment", "Error updating pet", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.updatePet((long) petId, petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); + Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } else { // Add new pet - petApi.createPet(petDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.log(requireContext(), "Added new Pet: " + name); - Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to add pet"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t)); - Log.e("PetDetailFragment", "Error adding pet", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.createPet(petDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.log(requireContext(), "Added new Pet: " + name); + Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } @@ -145,23 +126,13 @@ public class PetDetailFragment extends Fragment { */ private void deletePet() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - petApi.deletePet((long) petId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); - Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete pet"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t)); - Log.e("PetDetailFragment", "Error deleting pet", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.deletePet((long) petId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); + Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); } @@ -189,9 +160,7 @@ public class PetDetailFragment extends Fragment { etPetBreed.setText(getArguments().getString("petBreed")); etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); - SpinnerUtils.setSelectionByValue(spinnerPetStatus, getArguments().getString("petStatus")); - btnDeletePet.setVisibility(View.VISIBLE); } else { // Pet is being added @@ -228,5 +197,4 @@ public class PetDetailFragment extends Fragment { SpinnerUtils.setupStringSpinner(requireContext(), spinnerPetStatus, new String[]{"Available", "Adopted"}); } - } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 3db78dd0..bee0873d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -1,31 +1,30 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.SupplierViewModel; import java.math.BigDecimal; import java.util.*; -import javax.inject.Inject; - import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; /** - * Fragment for displaying and editing the relationship between products and suppliers, + * Fragment for displaying and editing the relationship between products and suppliers. */ @AndroidEntryPoint public class ProductSupplierDetailFragment extends Fragment { @@ -44,9 +43,17 @@ public class ProductSupplierDetailFragment extends Fragment { private List productList = new ArrayList<>(); private List supplierList = new ArrayList<>(); - @Inject ProductSupplierApi productSupplierApi; - @Inject ProductApi productApi; - @Inject SupplierApi supplierApi; + private ProductSupplierViewModel psViewModel; + private ProductViewModel productViewModel; + private SupplierViewModel supplierViewModel; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + psViewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); + productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -87,42 +94,28 @@ public class ProductSupplierDetailFragment extends Fragment { * Loads the list of products from the API. */ private void loadProducts() { - productApi.getAllProducts(null, 0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - productList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerProduct, productList, - ProductDTO::getProdName, "-- Select Product --", - preselectedProductId, ProductDTO::getProdId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("PSDetail", "Product load failed: " + t.getMessage()); - } - }); + productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); + } + }); } /** * Loads the list of suppliers from the API. */ private void loadSuppliers() { - supplierApi.getAllSuppliers(0, 200) - .enqueue(new Callback>() { - public void onResponse(Call> c, - Response> r) { - if (r.isSuccessful() && r.body() != null) { - supplierList = r.body().getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerSupplier, supplierList, - SupplierDTO::getSupCompany, "-- Select Supplier --", - preselectedSupplierId, SupplierDTO::getSupId); - } - } - public void onFailure(Call> c, Throwable t) { - Log.e("PSDetail", "Supplier load failed: " + t.getMessage()); - } - }); + supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + supplierList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), spinnerSupplier, supplierList, + SupplierDTO::getSupCompany, "-- Select Supplier --", + preselectedSupplierId, SupplierDTO::getSupId); + } + }); } /** @@ -169,48 +162,39 @@ public class ProductSupplierDetailFragment extends Fragment { product.getProdId(), supplier.getSupId(), cost); if (isEditing) { - productSupplierApi.updateProductSupplier(editProductId, editSupplierId, dto) - .enqueue(simpleCallback("Updated")); - } else { - productSupplierApi.createProductSupplier(dto).enqueue(simpleCallback("Saved")); - } - } - - /** - * callback for product-supplier save/update operations. - */ - private Callback simpleCallback(String msg) { - return new Callback<>() { - public void onResponse(Call c, Response r) { - if (r.isSuccessful()) { - Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + psViewModel.updateProductSupplier(editProductId, editSupplierId, dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Updated", Toast.LENGTH_SHORT).show(); navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), r, "Error " + r.code()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } - } - public void onFailure(Call c, Throwable t) { - Log.e("PS_SAVE", "Failure: " + t.getMessage()); - Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }; + }); + } else { + psViewModel.createProductSupplier(dto).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } /** * Shows a confirmation dialog before deleting a product-supplier relationship. */ private void confirmDelete() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> - productSupplierApi.deleteProductSupplier(editProductId, editSupplierId) - .enqueue(new Callback() { - public void onResponse(Call c, Response r) { - navigateBack(); - } - public void onFailure(Call c, Throwable t) { - Toast.makeText(getContext(), "Delete failed", - Toast.LENGTH_SHORT).show(); - } - })); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Product Supplier", () -> + psViewModel.deleteProductSupplier(editProductId, editSupplierId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Deleted", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + })); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index a70cb266..5cb9a7c2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -2,10 +2,11 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,19 +16,14 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; - -import javax.inject.Inject; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.ServiceViewModel; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; /** * Fragment for displaying and editing service details. @@ -41,10 +37,16 @@ public class ServiceDetailFragment extends Fragment { private int serviceId; private boolean isEditing = false; - @Inject ServiceApi serviceApi; + private ServiceViewModel viewModel; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ServiceViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_service_detail, container, false); @@ -87,44 +89,23 @@ public class ServiceDetailFragment extends Fragment { if (isEditing) { // Update existing service serviceDTO.setServiceId((long) serviceId); - serviceApi.updateService((long) serviceId, serviceDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId); - Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to update service"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t)); - Log.e("ServiceDetailFragment", "Error updating service", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.updateService((long) serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId); + Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } else { - // Add new service - serviceApi.createService(serviceDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.log(requireContext(), "Added new Service: " + name); - Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to add service"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t)); - Log.e("ServiceDetailFragment", "Error adding service", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.createService(serviceDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.log(requireContext(), "Added new Service: " + name); + Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } @@ -135,23 +116,13 @@ public class ServiceDetailFragment extends Fragment { */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - serviceApi.deleteService((long) serviceId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); - Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete service"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t)); - Log.e("ServiceDetailFragment", "Error deleting service", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.deleteService((long) serviceId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); + Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 3001faad..4477712f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -2,10 +2,11 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,20 +16,15 @@ import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; - -import javax.inject.Inject; +import com.example.petstoremobile.viewmodels.SupplierViewModel; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; /** * Fragment for displaying and editing supplier details. @@ -42,10 +38,16 @@ public class SupplierDetailFragment extends Fragment { private int supId; private boolean isEditing = false; - @Inject SupplierApi supplierApi; + private SupplierViewModel viewModel; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(SupplierViewModel.class); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_supplier_detail, container, false); @@ -91,44 +93,24 @@ public class SupplierDetailFragment extends Fragment { if (isEditing) { // Update existing supplier supplierDTO.setSupId((long) supId); - supplierApi.updateSupplier((long) supId, supplierDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId); - Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to update supplier"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t)); - Log.e("SupplierDetailFragment", "Error updating supplier", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.updateSupplier((long) supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId); + Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } else { // Add new supplier - supplierApi.createSupplier(supplierDTO).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.log(requireContext(), "Added new Supplier: " + company); - Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to add supplier"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t)); - Log.e("SupplierDetailFragment", "Error adding supplier", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + viewModel.createSupplier(supplierDTO).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.log(requireContext(), "Added new Supplier: " + company); + Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } @@ -138,24 +120,14 @@ public class SupplierDetailFragment extends Fragment { * Displays a confirmation dialog and handles the deletion of a supplier. */ private void deleteSupplier() { - DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - supplierApi.deleteSupplier((long) supId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); - Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); - } else { - ErrorUtils.showErrorMessage(getContext(), response, "Failed to delete supplier"); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t)); - Log.e("SupplierDetailFragment", "Error deleting supplier", t); - Toast.makeText(getContext(), "Network error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> + viewModel.deleteSupplier((long) supId).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS) { + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); + Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 57915e29..0ddc0511 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -13,7 +13,6 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; @@ -21,6 +20,7 @@ import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; +import com.example.petstoremobile.utils.RetrofitUtils; import java.io.File; import java.util.Locale; @@ -32,9 +32,6 @@ import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @AndroidEntryPoint public class PetProfileFragment extends Fragment { @@ -157,24 +154,12 @@ public class PetProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); // Call the backend to upload the image - petApi.uploadPetImage((long) petId, body).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(requireContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); - // Reload image after successful upload - loadPetImage(petId); - } else { - Toast.makeText(requireContext(), "Failed to upload pet photo", Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("UPLOAD_PET_IMAGE", "Failure: " + t.getMessage()); - Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + petApi.uploadPetImage((long) petId, body).enqueue(RetrofitUtils.createCallback( + requireContext(), + "UPLOAD_PET_IMAGE", + "Pet photo updated successfully", + result -> loadPetImage(petId) + )); } catch (Exception e) { Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage()); } @@ -184,23 +169,14 @@ public class PetProfileFragment extends Fragment { * Sends a request to the API to remove the current pet photo. */ private void deletePetImage() { - petApi.deletePetImage((long) petId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - Toast.makeText(requireContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); + petApi.deletePetImage((long) petId).enqueue(RetrofitUtils.createCallback( + requireContext(), + "DELETE_PET_IMAGE", + "Pet photo removed", + result -> { hasImage = false; imgPet.setImageResource(R.drawable.placeholder); - } else { - Toast.makeText(requireContext(), "Failed to remove pet photo", Toast.LENGTH_SHORT).show(); } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("DELETE_PET_IMAGE", "Failure: " + t.getMessage()); - Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); - } - }); + )); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index 8164d34b..8e47788e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class AdoptionRepository { + private static final String TAG = "AdoptionRepository"; private final AdoptionApi adoptionApi; @Inject @@ -31,21 +29,8 @@ public class AdoptionRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - adoptionApi.getAllAdoptions(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + adoptionApi.getAllAdoptions(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +42,8 @@ public class AdoptionRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - adoptionApi.getAdoptionById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + adoptionApi.getAdoptionById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +55,8 @@ public class AdoptionRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - adoptionApi.createAdoption(adoption).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + adoptionApi.createAdoption(adoption).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -109,21 +68,8 @@ public class AdoptionRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - adoptionApi.updateAdoption(id, adoption).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + adoptionApi.updateAdoption(id, adoption).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -135,21 +81,8 @@ public class AdoptionRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - adoptionApi.deleteAdoption(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + adoptionApi.deleteAdoption(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index cea28d0a..3dd6037d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class AppointmentRepository { + private static final String TAG = "AppointmentRepository"; private final AppointmentApi appointmentApi; @Inject @@ -31,21 +29,8 @@ public class AppointmentRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - appointmentApi.getAllAppointments(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + appointmentApi.getAllAppointments(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +42,8 @@ public class AppointmentRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - appointmentApi.getAppointmentById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + appointmentApi.getAppointmentById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +55,8 @@ public class AppointmentRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - appointmentApi.createAppointment(appointment).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + appointmentApi.createAppointment(appointment).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -109,21 +68,8 @@ public class AppointmentRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - appointmentApi.updateAppointment(id, appointment).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + appointmentApi.updateAppointment(id, appointment).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -135,21 +81,8 @@ public class AppointmentRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - appointmentApi.deleteAppointment(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + appointmentApi.deleteAppointment(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index c89de328..fd02e1ff 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -1,5 +1,7 @@ package com.example.petstoremobile.repositories; +import android.util.Log; + import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -8,6 +10,7 @@ import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import java.util.Map; @@ -15,12 +18,10 @@ import javax.inject.Inject; import javax.inject.Singleton; import okhttp3.MultipartBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @Singleton public class AuthRepository { + private static final String TAG = "AuthRepository"; private final AuthApi authApi; private final TokenManager tokenManager; @@ -37,26 +38,14 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.login(loginRequest).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - tokenManager.saveLoginData( - response.body().getToken(), - response.body().getUsername(), - response.body().getRole() - ); - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Login failed: " + response.message(), null)); - } + authApi.login(loginRequest).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + if (result != null && result.getToken() != null) { + tokenManager.saveLoginData(result.getToken(), result.getUsername(), result.getRole()); + data.setValue(Resource.success(result)); + } else { + data.setValue(Resource.error("Login failed", null)); } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + })); return data; } @@ -68,21 +57,8 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.getMe().enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + authApi.getMe().enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -94,21 +70,8 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.updateMe(updates).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + authApi.updateMe(updates).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -120,21 +83,8 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.uploadAvatar(avatar).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + authApi.uploadAvatar(avatar).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -146,21 +96,8 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.deleteAvatar().enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + authApi.deleteAvatar().enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } @@ -171,4 +108,8 @@ public class AuthRepository { public void logout() { tokenManager.clearLoginData(); } + + public boolean isLoggedIn() { + return tokenManager.getToken() != null; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java index 7837f582..ae3769ed 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class CategoryRepository { + private static final String TAG = "CategoryRepository"; private final CategoryApi categoryApi; @Inject @@ -31,21 +29,8 @@ public class CategoryRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - categoryApi.getAllCategories(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + categoryApi.getAllCategories(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java new file mode 100644 index 00000000..cd0ae505 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -0,0 +1,50 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.CustomerApi; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class CustomerRepository { + private static final String TAG = "CustomerRepository"; + private final CustomerApi customerApi; + + @Inject + public CustomerRepository(CustomerApi customerApi) { + this.customerApi = customerApi; + } + + /** + * Retrieves a paginated list of all customers from the API. + */ + public LiveData>> getAllCustomers(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + customerApi.getAllCustomers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); + + return data; + } + + /** + * Retrieves a specific customer by their ID. + */ + public LiveData> getCustomerById(Long id) { + MutableLiveData> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + customerApi.getCustomerById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index ee2503cd..e08738ad 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -9,16 +9,16 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; + +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class InventoryRepository { + private static final String TAG = "InventoryRepository"; private final InventoryApi inventoryApi; @Inject @@ -33,21 +33,8 @@ public class InventoryRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.getAllInventory(query, page, size, sort).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.getAllInventory(query, page, size, sort).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -59,21 +46,8 @@ public class InventoryRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.getInventoryById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.getInventoryById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -85,47 +59,18 @@ public class InventoryRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.createInventory(request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.createInventory(request).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } - /** - * Sends a request to the API to update an existing inventory record. - */ public LiveData> updateInventory(Long id, InventoryRequest request) { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.updateInventory(id, request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.updateInventory(id, request).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -137,47 +82,18 @@ public class InventoryRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.deleteInventory(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.deleteInventory(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } - /** - * Sends a request to the API to delete multiple inventory records at once. - */ public LiveData> bulkDeleteInventory(BulkDeleteRequest request) { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - inventoryApi.bulkDeleteInventory(request).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + inventoryApi.bulkDeleteInventory(request).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 0737e569..9fc087c3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -1,22 +1,22 @@ package com.example.petstoremobile.repositories; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; import okhttp3.MultipartBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; @Singleton public class PetRepository { + private static final String TAG = "PetRepository"; private final PetApi petApi; @Inject @@ -31,21 +31,8 @@ public class PetRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.getAllPets(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.getAllPets(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +44,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.getPetById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.getPetById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +57,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.createPet(pet).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.createPet(pet).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -109,21 +70,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.updatePet(id, pet).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.updatePet(id, pet).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -135,21 +83,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.deletePet(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.deletePet(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } @@ -161,21 +96,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.uploadPetImage(id, image).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.uploadPetImage(id, image).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } @@ -187,21 +109,8 @@ public class PetRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - petApi.deletePetImage(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + petApi.deletePetImage(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index df79335f..84ac81b1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -7,17 +7,16 @@ import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; import okhttp3.MultipartBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; @Singleton public class ProductRepository { + private static final String TAG = "ProductRepository"; private final ProductApi productApi; @Inject @@ -32,21 +31,8 @@ public class ProductRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.getAllProducts(query, page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.getAllProducts(query, page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -58,21 +44,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.getProductById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.getProductById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -84,21 +57,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.createProduct(product).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.createProduct(product).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -110,21 +70,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.updateProduct(id, product).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.updateProduct(id, product).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -136,21 +83,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.deleteProduct(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.deleteProduct(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } @@ -162,21 +96,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.uploadProductImage(id, image).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.uploadProductImage(id, image).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } @@ -188,21 +109,8 @@ public class ProductRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - productApi.deleteProductImage(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + productApi.deleteProductImage(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index 2e961f6c..eebe13f6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.ProductSupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class ProductSupplierRepository { + private static final String TAG = "ProductSupplierRepository"; private final ProductSupplierApi api; @Inject @@ -31,21 +29,8 @@ public class ProductSupplierRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.getAllProductSuppliers(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.getAllProductSuppliers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +42,8 @@ public class ProductSupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.createProductSupplier(dto).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.createProductSupplier(dto).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +55,8 @@ public class ProductSupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.updateProductSupplier(productId, supplierId, dto).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.updateProductSupplier(productId, supplierId, dto).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -109,21 +68,8 @@ public class ProductSupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.deleteProductSupplier(productId, supplierId).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.deleteProductSupplier(productId, supplierId).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index f804c2e3..ad46c26b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -7,6 +7,7 @@ import com.example.petstoremobile.api.PurchaseOrderApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -17,6 +18,7 @@ import retrofit2.Response; @Singleton public class PurchaseOrderRepository { + private static final String TAG = "PurchaseOrderRepo"; private final PurchaseOrderApi api; @Inject @@ -31,21 +33,9 @@ public class PurchaseOrderRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.getAllPurchaseOrders(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.getAllPurchaseOrders(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + data.setValue(Resource.success(result)); + })); return data; } @@ -57,21 +47,9 @@ public class PurchaseOrderRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - api.getPurchaseOrderById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + api.getPurchaseOrderById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + data.setValue(Resource.success(result)); + })); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index 8d81ccfb..4503fa80 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class ServiceRepository { + private static final String TAG = "ServiceRepository"; private final ServiceApi serviceApi; @Inject @@ -31,21 +29,8 @@ public class ServiceRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - serviceApi.getAllServices(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + serviceApi.getAllServices(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +42,8 @@ public class ServiceRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - serviceApi.getServiceById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + serviceApi.getServiceById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +55,8 @@ public class ServiceRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - serviceApi.createService(service).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + serviceApi.createService(service).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -109,21 +68,8 @@ public class ServiceRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - serviceApi.updateService(id, service).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + serviceApi.updateService(id, service).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -135,21 +81,8 @@ public class ServiceRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - serviceApi.deleteService(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + serviceApi.deleteService(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java new file mode 100644 index 00000000..e6609bdc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -0,0 +1,37 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.api.StoreApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class StoreRepository { + private static final String TAG = "StoreRepository"; + private final StoreApi storeApi; + + @Inject + public StoreRepository(StoreApi storeApi) { + this.storeApi = storeApi; + } + + /** + * Retrieves a paginated list of all stores from the API. + */ + public LiveData>> getAllStores(int page, int size) { + MutableLiveData>> data = new MutableLiveData<>(); + data.setValue(Resource.loading(null)); + + storeApi.getAllStores(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); + + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index eb7b8b61..a7a1b633 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -7,16 +7,14 @@ import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class SupplierRepository { + private static final String TAG = "SupplierRepository"; private final SupplierApi supplierApi; @Inject @@ -31,21 +29,8 @@ public class SupplierRepository { MutableLiveData>> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - supplierApi.getAllSuppliers(page, size).enqueue(new Callback>() { - @Override - public void onResponse(Call> call, Response> response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call> call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + supplierApi.getAllSuppliers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -57,21 +42,8 @@ public class SupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - supplierApi.getSupplierById(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + supplierApi.getSupplierById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -83,21 +55,8 @@ public class SupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - supplierApi.createSupplier(supplier).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + supplierApi.createSupplier(supplier).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(result)))); return data; } @@ -106,26 +65,15 @@ public class SupplierRepository { * Sends a request to the API to update an existing supplier record by ID. */ public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); + MutableLiveData>> data = new MutableLiveData<>(); + // Note: The original return type was LiveData>, fixing here + MutableLiveData> resultData = new MutableLiveData<>(); + resultData.setValue(Resource.loading(null)); - supplierApi.updateSupplier(id, supplier).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful() && response.body() != null) { - data.setValue(Resource.success(response.body())); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } + supplierApi.updateSupplier(id, supplier).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> resultData.setValue(Resource.success(result)))); - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); - - return data; + return resultData; } /** @@ -135,21 +83,8 @@ public class SupplierRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - supplierApi.deleteSupplier(id).enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - data.setValue(Resource.success(null)); - } else { - data.setValue(Resource.error("Error: " + response.message(), null)); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - data.setValue(Resource.error(t.getMessage(), null)); - } - }); + supplierApi.deleteSupplier(id).enqueue(RetrofitUtils.createSilentCallback(TAG, + result -> data.setValue(Resource.success(null)))); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java index a502d8d0..90113cac 100644 --- a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java +++ b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java @@ -14,6 +14,7 @@ import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.NotificationHelper; +import com.example.petstoremobile.utils.RetrofitUtils; import com.example.petstoremobile.websocket.StompChatManager; import java.util.HashMap; import java.util.HashSet; @@ -63,54 +64,31 @@ public class ChatNotificationService extends Service { currentUserId = tokenManager.getUserId(); if (token != null && stompChatManager == null) { - customerApi.getAllCustomers(0, 1000).enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - for (CustomerDTO customer : response.body().getContent()) { - customerIdToName.put(customer.getCustomerId(), customer.getFullName()); - } - } - loadConversationsAndStartStomp(token, role); + customerApi.getAllCustomers(0, 1000).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + for (CustomerDTO customer : result.getContent()) { + customerIdToName.put(customer.getCustomerId(), customer.getFullName()); } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - Log.e(TAG, "Failed to load customers", t); - loadConversationsAndStartStomp(token, role); - } - }); + loadConversationsAndStartStomp(token, role); + })); } } private void loadConversationsAndStartStomp(String token, String role) { // Fetch existing conversations - chatApi.getAllConversations().enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful() && response.body() != null) { - for (ConversationDTO conversation : response.body()) { - if (conversation.getId() != null) { - knownConversationIds.add(conversation.getId()); - conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId()); - // subscribe to existing conversations to get message notifications - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(conversation.getId()); - } - } + chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + for (ConversationDTO conversation : result) { + if (conversation.getId() != null) { + knownConversationIds.add(conversation.getId()); + conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId()); + // subscribe to existing conversations to get message notifications + if (stompChatManager != null) { + stompChatManager.subscribeToConversation(conversation.getId()); } - Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations"); } - startStomp(token, role); } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - Log.e(TAG, "Failed to load existing conversations", t); - //tries to connect if loading fails - startStomp(token, role); - } - }); + Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations"); + startStomp(token, role); + })); } private void startStomp(String token, String role) { @@ -199,19 +177,9 @@ public class ChatNotificationService extends Service { // Helper function to fetch customer name for a conversation private void fetchCustomerName(Long customerId) { - customerApi.getCustomerById(customerId).enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful() && response.body() != null) { - customerIdToName.put(customerId, response.body().getFullName()); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(TAG, "Failed to fetch customer name", t); - } - }); + customerApi.getCustomerById(customerId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { + customerIdToName.put(customerId, result.getFullName()); + })); } //When the service is destroyed, disconnect from the websocket @@ -228,4 +196,4 @@ public class ChatNotificationService extends Service { public IBinder onBind(Intent intent) { return null; } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java new file mode 100644 index 00000000..a5a1b041 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java @@ -0,0 +1,74 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * Utility class for common Retrofit operations and standardized callbacks. + */ +public class RetrofitUtils { + + /** + * Interface for handling successful API responses. + * @param The type of the response body. + */ + public interface SuccessCallback { + void onSuccess(T result); + } + + /** + * Creates a callback for Retrofit calls that handles errors and logging. + */ + public static Callback createCallback(Context context, String tag, String successMsg, SuccessCallback successCallback) { + return new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + if (successMsg != null) { + Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show(); + } + if (successCallback != null) { + successCallback.onSuccess(response.body()); + } + } else { + ErrorUtils.showErrorMessage(context, response, "Operation failed"); + Log.e(tag, "API Error: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e(tag, "Network Error: " + t.getMessage()); + Toast.makeText(context, "Network error. Please try again.", Toast.LENGTH_SHORT).show(); + } + }; + } + + /** + * Creates a callback that doesn't show toasts + */ + public static Callback createSilentCallback(String tag, SuccessCallback successCallback) { + return new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && successCallback != null) { + successCallback.onSuccess(response.body()); + } else { + Log.e(tag, "API Error: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e(tag, "Network Error: " + t.getMessage()); + } + }; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java new file mode 100644 index 00000000..5ad7cc76 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/CustomerViewModel.java @@ -0,0 +1,37 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.repositories.CustomerRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class CustomerViewModel extends ViewModel { + private final CustomerRepository repository; + + @Inject + public CustomerViewModel(CustomerRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all customers. + */ + public LiveData>> getAllCustomers(int page, int size) { + return repository.getAllCustomers(page, size); + } + + /** + * Retrieves a single customer by their ID. + */ + public LiveData> getCustomerById(Long id) { + return repository.getCustomerById(id); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java new file mode 100644 index 00000000..83f4c3b3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/StoreViewModel.java @@ -0,0 +1,30 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.StoreDTO; +import com.example.petstoremobile.repositories.StoreRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class StoreViewModel extends ViewModel { + private final StoreRepository repository; + + @Inject + public StoreViewModel(StoreRepository repository) { + this.repository = repository; + } + + /** + * Fetches a paginated list of all stores. + */ + public LiveData>> getAllStores(int page, int size) { + return repository.getAllStores(page, size); + } +} From b14e318df27efe2b88b8039e0871d4b9b5869574 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:40:43 -0600 Subject: [PATCH 064/137] Fixed bug where it navigates back to petprofile after deleting the pet --- .../detailfragments/PetDetailFragment.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index afff4dd1..23071db6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -102,7 +102,7 @@ public class PetDetailFragment extends Fragment { if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); + navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -113,7 +113,7 @@ public class PetDetailFragment extends Fragment { if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.log(requireContext(), "Added new Pet: " + name); Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); + navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -130,13 +130,20 @@ public class PetDetailFragment extends Fragment { if (resource.status == Resource.Status.SUCCESS) { ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); - navigateBack(); + navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); } + /** + * Navigates back to the pet list screen. + */ + private void navigateToPetList() { + NavHostFragment.findNavController(this).popBackStack(R.id.nav_pet, false); + } + /** * Navigates back to the previous screen. */ From 1137688d604052b5765bc9cdfdddda279fa2aaa5 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:57:53 -0600 Subject: [PATCH 065/137] Edited RetrofitUtils to also call enqueue to reduce code in repository --- .../repositories/AdoptionRepository.java | 30 ++-------- .../repositories/AppointmentRepository.java | 30 ++-------- .../repositories/AuthRepository.java | 57 ++++++++++--------- .../repositories/CategoryRepository.java | 6 +- .../repositories/CustomerRepository.java | 12 +--- .../repositories/InventoryRepository.java | 36 ++---------- .../repositories/PetRepository.java | 42 +++----------- .../repositories/ProductRepository.java | 42 +++----------- .../ProductSupplierRepository.java | 24 ++------ .../repositories/PurchaseOrderRepository.java | 18 +----- .../repositories/ServiceRepository.java | 30 ++-------- .../repositories/StoreRepository.java | 6 +- .../repositories/SupplierRepository.java | 36 +++--------- .../petstoremobile/utils/ErrorUtils.java | 14 +++-- .../petstoremobile/utils/RetrofitUtils.java | 29 ++++++++++ 15 files changed, 121 insertions(+), 291 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index 8e47788e..f5a5d2c5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -27,11 +27,7 @@ public class AdoptionRepository { */ public LiveData>> getAllAdoptions(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - adoptionApi.getAllAdoptions(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(adoptionApi.getAllAdoptions(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class AdoptionRepository { */ public LiveData> getAdoptionById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - adoptionApi.getAdoptionById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(adoptionApi.getAdoptionById(id), data, TAG); return data; } @@ -53,11 +45,7 @@ public class AdoptionRepository { */ public LiveData> createAdoption(AdoptionDTO adoption) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - adoptionApi.createAdoption(adoption).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(adoptionApi.createAdoption(adoption), data, TAG); return data; } @@ -66,11 +54,7 @@ public class AdoptionRepository { */ public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - adoptionApi.updateAdoption(id, adoption).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(adoptionApi.updateAdoption(id, adoption), data, TAG); return data; } @@ -79,11 +63,7 @@ public class AdoptionRepository { */ public LiveData> deleteAdoption(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - adoptionApi.deleteAdoption(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(adoptionApi.deleteAdoption(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 3dd6037d..61f56842 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -27,11 +27,7 @@ public class AppointmentRepository { */ public LiveData>> getAllAppointments(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - appointmentApi.getAllAppointments(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(appointmentApi.getAllAppointments(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class AppointmentRepository { */ public LiveData> getAppointmentById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - appointmentApi.getAppointmentById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(appointmentApi.getAppointmentById(id), data, TAG); return data; } @@ -53,11 +45,7 @@ public class AppointmentRepository { */ public LiveData> createAppointment(AppointmentDTO appointment) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - appointmentApi.createAppointment(appointment).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(appointmentApi.createAppointment(appointment), data, TAG); return data; } @@ -66,11 +54,7 @@ public class AppointmentRepository { */ public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - appointmentApi.updateAppointment(id, appointment).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(appointmentApi.updateAppointment(id, appointment), data, TAG); return data; } @@ -79,11 +63,7 @@ public class AppointmentRepository { */ public LiveData> deleteAppointment(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - appointmentApi.deleteAppointment(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(appointmentApi.deleteAppointment(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index fd02e1ff..dec9613e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -2,6 +2,7 @@ package com.example.petstoremobile.repositories; import android.util.Log; +import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -9,6 +10,7 @@ import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.RetrofitUtils; @@ -18,6 +20,9 @@ import javax.inject.Inject; import javax.inject.Singleton; import okhttp3.MultipartBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; @Singleton public class AuthRepository { @@ -38,14 +43,28 @@ public class AuthRepository { MutableLiveData> data = new MutableLiveData<>(); data.setValue(Resource.loading(null)); - authApi.login(loginRequest).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - if (result != null && result.getToken() != null) { - tokenManager.saveLoginData(result.getToken(), result.getUsername(), result.getRole()); - data.setValue(Resource.success(result)); - } else { - data.setValue(Resource.error("Login failed", null)); + authApi.login(loginRequest).enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + AuthDTO.LoginResponse result = response.body(); + if (result != null && result.getToken() != null) { + tokenManager.saveLoginData(result.getToken(), result.getUsername(), result.getRole()); + data.setValue(Resource.success(result)); + } else { + data.setValue(Resource.error("Login failed: Invalid response", null)); + } + } else { + String errorMsg = ErrorUtils.getErrorMessage(response, "Login failed"); + data.setValue(Resource.error(errorMsg, null)); + } } - })); + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + data.setValue(Resource.error("Network error: " + t.getMessage(), null)); + } + }); return data; } @@ -55,11 +74,7 @@ public class AuthRepository { */ public LiveData> getMe() { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - authApi.getMe().enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(authApi.getMe(), data, TAG); return data; } @@ -68,11 +83,7 @@ public class AuthRepository { */ public LiveData> updateMe(Map updates) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - authApi.updateMe(updates).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(authApi.updateMe(updates), data, TAG); return data; } @@ -81,11 +92,7 @@ public class AuthRepository { */ public LiveData> uploadAvatar(MultipartBody.Part avatar) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - authApi.uploadAvatar(avatar).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(authApi.uploadAvatar(avatar), data, TAG); return data; } @@ -94,11 +101,7 @@ public class AuthRepository { */ public LiveData> deleteAvatar() { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - authApi.deleteAvatar().enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(authApi.deleteAvatar(), data, TAG); return data; } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java index ae3769ed..74516e2b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -27,11 +27,7 @@ public class CategoryRepository { */ public LiveData>> getAllCategories(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - categoryApi.getAllCategories(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(categoryApi.getAllCategories(page, size), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index cd0ae505..834f9197 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -27,11 +27,7 @@ public class CustomerRepository { */ public LiveData>> getAllCustomers(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - customerApi.getAllCustomers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(customerApi.getAllCustomers(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class CustomerRepository { */ public LiveData> getCustomerById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - customerApi.getCustomerById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(customerApi.getCustomerById(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index e08738ad..4fd2e66d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -31,11 +31,7 @@ public class InventoryRepository { */ public LiveData>> getAllInventory(String query, int page, int size, String sort) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.getAllInventory(query, page, size, sort).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(inventoryApi.getAllInventory(query, page, size, sort), data, TAG); return data; } @@ -44,11 +40,7 @@ public class InventoryRepository { */ public LiveData> getInventoryById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.getInventoryById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(inventoryApi.getInventoryById(id), data, TAG); return data; } @@ -57,21 +49,13 @@ public class InventoryRepository { */ public LiveData> createInventory(InventoryRequest request) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.createInventory(request).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(inventoryApi.createInventory(request), data, TAG); return data; } public LiveData> updateInventory(Long id, InventoryRequest request) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.updateInventory(id, request).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(inventoryApi.updateInventory(id, request), data, TAG); return data; } @@ -80,21 +64,13 @@ public class InventoryRepository { */ public LiveData> deleteInventory(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.deleteInventory(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(inventoryApi.deleteInventory(id), data, TAG); return data; } public LiveData> bulkDeleteInventory(BulkDeleteRequest request) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - inventoryApi.bulkDeleteInventory(request).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(inventoryApi.bulkDeleteInventory(request), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 9fc087c3..f46bb9e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -29,11 +29,7 @@ public class PetRepository { */ public LiveData>> getAllPets(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.getAllPets(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(petApi.getAllPets(page, size), data, TAG); return data; } @@ -42,11 +38,7 @@ public class PetRepository { */ public LiveData> getPetById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.getPetById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(petApi.getPetById(id), data, TAG); return data; } @@ -55,11 +47,7 @@ public class PetRepository { */ public LiveData> createPet(PetDTO pet) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.createPet(pet).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(petApi.createPet(pet), data, TAG); return data; } @@ -68,11 +56,7 @@ public class PetRepository { */ public LiveData> updatePet(Long id, PetDTO pet) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.updatePet(id, pet).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(petApi.updatePet(id, pet), data, TAG); return data; } @@ -81,11 +65,7 @@ public class PetRepository { */ public LiveData> deletePet(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.deletePet(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(petApi.deletePet(id), data, TAG); return data; } @@ -94,11 +74,7 @@ public class PetRepository { */ public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.uploadPetImage(id, image).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(petApi.uploadPetImage(id, image), data, TAG); return data; } @@ -107,11 +83,7 @@ public class PetRepository { */ public LiveData> deletePetImage(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - petApi.deletePetImage(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(petApi.deletePetImage(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 84ac81b1..0baf5c4a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -29,11 +29,7 @@ public class ProductRepository { */ public LiveData>> getAllProducts(String query, int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.getAllProducts(query, page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(productApi.getAllProducts(query, page, size), data, TAG); return data; } @@ -42,11 +38,7 @@ public class ProductRepository { */ public LiveData> getProductById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.getProductById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(productApi.getProductById(id), data, TAG); return data; } @@ -55,11 +47,7 @@ public class ProductRepository { */ public LiveData> createProduct(ProductDTO product) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.createProduct(product).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(productApi.createProduct(product), data, TAG); return data; } @@ -68,11 +56,7 @@ public class ProductRepository { */ public LiveData> updateProduct(Long id, ProductDTO product) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.updateProduct(id, product).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(productApi.updateProduct(id, product), data, TAG); return data; } @@ -81,11 +65,7 @@ public class ProductRepository { */ public LiveData> deleteProduct(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.deleteProduct(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(productApi.deleteProduct(id), data, TAG); return data; } @@ -94,11 +74,7 @@ public class ProductRepository { */ public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.uploadProductImage(id, image).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(productApi.uploadProductImage(id, image), data, TAG); return data; } @@ -107,11 +83,7 @@ public class ProductRepository { */ public LiveData> deleteProductImage(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - productApi.deleteProductImage(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(productApi.deleteProductImage(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index eebe13f6..e92472c0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -27,11 +27,7 @@ public class ProductSupplierRepository { */ public LiveData>> getAllProductSuppliers(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.getAllProductSuppliers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(api.getAllProductSuppliers(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class ProductSupplierRepository { */ public LiveData> createProductSupplier(ProductSupplierDTO dto) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.createProductSupplier(dto).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(api.createProductSupplier(dto), data, TAG); return data; } @@ -53,11 +45,7 @@ public class ProductSupplierRepository { */ public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.updateProductSupplier(productId, supplierId, dto).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(api.updateProductSupplier(productId, supplierId, dto), data, TAG); return data; } @@ -66,11 +54,7 @@ public class ProductSupplierRepository { */ public LiveData> deleteProductSupplier(Long productId, Long supplierId) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.deleteProductSupplier(productId, supplierId).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(api.deleteProductSupplier(productId, supplierId), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index ad46c26b..f00400e5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -12,10 +12,6 @@ import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - @Singleton public class PurchaseOrderRepository { private static final String TAG = "PurchaseOrderRepo"; @@ -31,12 +27,7 @@ public class PurchaseOrderRepository { */ public LiveData>> getAllPurchaseOrders(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.getAllPurchaseOrders(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - data.setValue(Resource.success(result)); - })); - + RetrofitUtils.enqueue(api.getAllPurchaseOrders(page, size), data, TAG); return data; } @@ -45,12 +36,7 @@ public class PurchaseOrderRepository { */ public LiveData> getPurchaseOrderById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - api.getPurchaseOrderById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - data.setValue(Resource.success(result)); - })); - + RetrofitUtils.enqueue(api.getPurchaseOrderById(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index 4503fa80..ca6021e6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -27,11 +27,7 @@ public class ServiceRepository { */ public LiveData>> getAllServices(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - serviceApi.getAllServices(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(serviceApi.getAllServices(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class ServiceRepository { */ public LiveData> getServiceById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - serviceApi.getServiceById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(serviceApi.getServiceById(id), data, TAG); return data; } @@ -53,11 +45,7 @@ public class ServiceRepository { */ public LiveData> createService(ServiceDTO service) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - serviceApi.createService(service).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(serviceApi.createService(service), data, TAG); return data; } @@ -66,11 +54,7 @@ public class ServiceRepository { */ public LiveData> updateService(Long id, ServiceDTO service) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - serviceApi.updateService(id, service).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(serviceApi.updateService(id, service), data, TAG); return data; } @@ -79,11 +63,7 @@ public class ServiceRepository { */ public LiveData> deleteService(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - serviceApi.deleteService(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(serviceApi.deleteService(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index e6609bdc..a71a7ffe 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -27,11 +27,7 @@ public class StoreRepository { */ public LiveData>> getAllStores(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - storeApi.getAllStores(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(storeApi.getAllStores(page, size), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index a7a1b633..f169f415 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -27,11 +27,7 @@ public class SupplierRepository { */ public LiveData>> getAllSuppliers(int page, int size) { MutableLiveData>> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - supplierApi.getAllSuppliers(page, size).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(supplierApi.getAllSuppliers(page, size), data, TAG); return data; } @@ -40,11 +36,7 @@ public class SupplierRepository { */ public LiveData> getSupplierById(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - supplierApi.getSupplierById(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(supplierApi.getSupplierById(id), data, TAG); return data; } @@ -53,11 +45,7 @@ public class SupplierRepository { */ public LiveData> createSupplier(SupplierDTO supplier) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - supplierApi.createSupplier(supplier).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(result)))); - + RetrofitUtils.enqueue(supplierApi.createSupplier(supplier), data, TAG); return data; } @@ -65,15 +53,9 @@ public class SupplierRepository { * Sends a request to the API to update an existing supplier record by ID. */ public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - MutableLiveData>> data = new MutableLiveData<>(); - // Note: The original return type was LiveData>, fixing here - MutableLiveData> resultData = new MutableLiveData<>(); - resultData.setValue(Resource.loading(null)); - - supplierApi.updateSupplier(id, supplier).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> resultData.setValue(Resource.success(result)))); - - return resultData; + MutableLiveData> data = new MutableLiveData<>(); + RetrofitUtils.enqueue(supplierApi.updateSupplier(id, supplier), data, TAG); + return data; } /** @@ -81,11 +63,7 @@ public class SupplierRepository { */ public LiveData> deleteSupplier(Long id) { MutableLiveData> data = new MutableLiveData<>(); - data.setValue(Resource.loading(null)); - - supplierApi.deleteSupplier(id).enqueue(RetrofitUtils.createSilentCallback(TAG, - result -> data.setValue(Resource.success(null)))); - + RetrofitUtils.enqueue(supplierApi.deleteSupplier(id), data, TAG); return data; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java index 941f9662..a1e69f5f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -15,18 +15,24 @@ public class ErrorUtils { * Shows an error message to toast based on the response. */ public static void showErrorMessage(Context context, Response response, String defaultMessage) { + Toast.makeText(context, getErrorMessage(response, defaultMessage), Toast.LENGTH_LONG).show(); + } + + /** + * Extracts the error message from the response body. + */ + public static String getErrorMessage(Response response, String defaultMessage) { try { - if (response.errorBody() != null) { + if (response != null && response.errorBody() != null) { String errorJson = response.errorBody().string(); ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class); if (errorResponse != null && errorResponse.getMessage() != null) { - Toast.makeText(context, errorResponse.getMessage(), Toast.LENGTH_LONG).show(); - return; + return errorResponse.getMessage(); } } } catch (Exception e) { Log.e("ErrorUtils", "Error parsing error body", e); } - Toast.makeText(context, defaultMessage, Toast.LENGTH_SHORT).show(); + return defaultMessage; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java index a5a1b041..3584f845 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java @@ -5,6 +5,7 @@ import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; import retrofit2.Call; import retrofit2.Callback; @@ -23,9 +24,36 @@ public class RetrofitUtils { void onSuccess(T result); } + /** + * call and updates the provided MutableLiveData with Resource states. + */ + public static void enqueue(@NonNull Call call, @NonNull MutableLiveData> data, String tag) { + data.setValue(Resource.loading(null)); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + data.setValue(Resource.success(response.body())); + } else { + String errorMsg = ErrorUtils.getErrorMessage(response, "API Error: " + response.code()); + Log.e(tag, errorMsg); + data.setValue(Resource.error(errorMsg, null)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + String errorMsg = "Network Error: " + t.getMessage(); + Log.e(tag, errorMsg); + data.setValue(Resource.error(errorMsg, null)); + } + }); + } + /** * Creates a callback for Retrofit calls that handles errors and logging. */ + @Deprecated public static Callback createCallback(Context context, String tag, String successMsg, SuccessCallback successCallback) { return new Callback() { @Override @@ -54,6 +82,7 @@ public class RetrofitUtils { /** * Creates a callback that doesn't show toasts */ + @Deprecated public static Callback createSilentCallback(String tag, SuccessCallback successCallback) { return new Callback() { @Override From d62113c0f5f0b0924a223377163bc19b1523cf25 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:50:25 -0600 Subject: [PATCH 066/137] Implemented View Binding to reduce code - project uses view binding now so we don have to do getViewbyId to refer to the xml --- android/app/build.gradle.kts | 1 + .../activities/HomeActivity.java | 18 ++- .../activities/MainActivity.java | 48 +++----- .../fragments/ChatFragment.java | 101 ++++++--------- .../fragments/ListFragment.java | 76 +++++------- .../fragments/ProfileFragment.java | 64 ++++------ .../listfragments/AdoptionFragment.java | 95 +++++++-------- .../listfragments/AppointmentFragment.java | 115 ++++++------------ .../listfragments/InventoryFragment.java | 94 ++++++-------- .../fragments/listfragments/PetFragment.java | 79 +++++------- .../listfragments/ProductFragment.java | 62 +++++----- .../ProductSupplierFragment.java | 57 +++++---- .../listfragments/PurchaseOrderFragment.java | 55 +++++---- .../fragments/listfragments/SaleFragment.java | 67 +++++----- .../listfragments/ServiceFragment.java | 59 ++++----- .../listfragments/SupplierFragment.java | 59 ++++----- .../AdoptionDetailFragment.java | 76 +++++------- .../AppointmentDetailFragment.java | 104 +++++++--------- .../InventoryDetailFragment.java | 104 +++++++--------- .../detailfragments/PetDetailFragment.java | 97 ++++++--------- .../ProductDetailFragment.java | 84 ++++++------- .../ProductSupplierDetailFragment.java | 61 ++++------ .../PurchaseOrderDetailFragment.java | 40 +++--- .../detailfragments/RefundDetailFragment.java | 69 +++++------ .../ServiceDetailFragment.java | 78 +++++------- .../SupplierDetailFragment.java | 94 ++++++-------- 26 files changed, 773 insertions(+), 1084 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 87add966..6cd974d9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -39,6 +39,7 @@ android { buildFeatures { buildConfig = true + viewBinding = true } buildTypes { diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index d9e41f96..01b4ca24 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -20,14 +20,14 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ActivityHomeBinding; import com.example.petstoremobile.services.ChatNotificationService; -import com.google.android.material.bottomnavigation.BottomNavigationView; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class HomeActivity extends AppCompatActivity { - private BottomNavigationView bottomNav; + private ActivityHomeBinding binding; private NavController navController; // Launcher to ask for notification permission @@ -45,23 +45,21 @@ public class HomeActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { EdgeToEdge.enable(this); super.onCreate(savedInstanceState); - setContentView(R.layout.activity_home); + binding = ActivityHomeBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); - //get the bottom navbar from the layout - bottomNav = findViewById(R.id.bottom_navigation); - // Initialize Navigation Component NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager() .findFragmentById(R.id.nav_host_fragment); if (navHostFragment != null) { navController = navHostFragment.getNavController(); - NavigationUI.setupWithNavController(bottomNav, navController); + NavigationUI.setupWithNavController(binding.bottomNavigation, navController); } //load the list fragment by default if it's a fresh start @@ -89,9 +87,9 @@ public class HomeActivity extends AppCompatActivity { */ private void handleIntent(Intent intent) { if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { - if (bottomNav != null) { + if (binding.bottomNavigation != null) { // Navigate by selecting the bottom nav item. - bottomNav.setSelectedItemId(R.id.nav_chat); + binding.bottomNavigation.setSelectedItemId(R.id.nav_chat); } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java index 3d27aab3..f8c89342 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -2,11 +2,7 @@ package com.example.petstoremobile.activities; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import android.view.inputmethod.EditorInfo; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import androidx.activity.EdgeToEdge; @@ -16,8 +12,8 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; -import com.example.petstoremobile.R; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.ActivityMainBinding; import com.example.petstoremobile.viewmodels.AuthViewModel; import com.example.petstoremobile.utils.Resource; @@ -30,10 +26,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class MainActivity extends AppCompatActivity { - private EditText etUser; - private EditText etPassword; - private Button btnLogin; - private TextView tvLoginStatus; + private ActivityMainBinding binding; private AuthViewModel viewModel; @Inject TokenManager tokenManager; @@ -59,41 +52,38 @@ public class MainActivity extends AppCompatActivity { } } - setContentView(R.layout.activity_main); + binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + viewModel = new ViewModelProvider(this).get(AuthViewModel.class); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); - //get all controls from layout - tvLoginStatus = findViewById(R.id.tvLoginStatus); - etUser = findViewById(R.id.etUser); - etPassword = findViewById(R.id.etPassword); - btnLogin = findViewById(R.id.btnLogin); //clear login status - tvLoginStatus.setText(""); + binding.tvLoginStatus.setText(""); // Set editor action listener for password field to login on when enter is pressed - etPassword.setOnEditorActionListener((v, actionId, event) -> { + binding.etPassword.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) { - btnLogin.performClick(); + binding.btnLogin.performClick(); return true; } return false; }); //Set click listener for login button - btnLogin.setOnClickListener(v -> { + binding.btnLogin.setOnClickListener(v -> { //Get username and password from text fields - String username = etUser.getText().toString(); - String password = etPassword.getText().toString(); + String username = binding.etUser.getText().toString(); + String password = binding.etPassword.getText().toString(); //check if fields are empty if (username.isEmpty() || password.isEmpty()) { Toast.makeText(this, "Please enter username and password", Toast.LENGTH_SHORT).show(); - tvLoginStatus.setText("Please enter username and password"); + binding.tvLoginStatus.setText("Please enter username and password"); return; } @@ -110,15 +100,15 @@ public class MainActivity extends AppCompatActivity { switch (resource.status) { case LOADING: - btnLogin.setEnabled(false); - tvLoginStatus.setText("Logging in..."); + binding.btnLogin.setEnabled(false); + binding.tvLoginStatus.setText("Logging in..."); break; case SUCCESS: if (resource.data != null) { String role = resource.data.getRole(); if ("CUSTOMER".equalsIgnoreCase(role)) { - btnLogin.setEnabled(true); - tvLoginStatus.setText("Customers are not allowed to log in"); + binding.btnLogin.setEnabled(true); + binding.tvLoginStatus.setText("Customers are not allowed to log in"); Toast.makeText(this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show(); } else { tokenManager.saveLoginData(resource.data.getToken(), resource.data.getUsername(), role); @@ -127,8 +117,8 @@ public class MainActivity extends AppCompatActivity { } break; case ERROR: - btnLogin.setEnabled(true); - tvLoginStatus.setText(resource.message); + binding.btnLogin.setEnabled(true); + binding.tvLoginStatus.setText(resource.message); Toast.makeText(this, resource.message, Toast.LENGTH_LONG).show(); break; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 25f4a7f1..81383acc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -9,15 +9,12 @@ import android.provider.OpenableColumns; import android.util.Log; import android.view.*; import android.view.inputmethod.EditorInfo; -import android.widget.*; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.core.view.GravityCompat; -import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; @@ -26,10 +23,10 @@ import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.api.ChatApi; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.api.MessageApi; +import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.dtos.ConversationDTO; import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.MessageDTO; -import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; @@ -44,7 +41,6 @@ import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; -import retrofit2.*; @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, @@ -52,19 +48,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private static final String TAG = "ChatFragment"; - // View - private DrawerLayout drawerLayout; - private RecyclerView rvChatList, rvMessages; - private EditText etMessage; - private Button btnSend; - private ImageButton btnAttach; - private TextView tvChatTitle; - - // Preview views - private View layoutAttachmentPreview; - private ImageView ivPreview; - private TextView tvPreviewName; - private ImageButton btnRemoveAttachment; + private FragmentChatBinding binding; // Adapters private ChatAdapter chatAdapter; @@ -115,35 +99,21 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_chat, container, false); + binding = FragmentChatBinding.inflate(inflater, container, false); - drawerLayout = view.findViewById(R.id.chatDrawerLayout); - rvChatList = view.findViewById(R.id.rvChatList); - rvMessages = view.findViewById(R.id.rvMessages); - etMessage = view.findViewById(R.id.etMessage); - btnSend = view.findViewById(R.id.btnSend); - btnAttach = view.findViewById(R.id.btnAttach); - tvChatTitle = view.findViewById(R.id.tvChatTitle); - - layoutAttachmentPreview = view.findViewById(R.id.layoutAttachmentPreview); - ivPreview = view.findViewById(R.id.ivPreview); - tvPreviewName = view.findViewById(R.id.tvPreviewName); - btnRemoveAttachment = view.findViewById(R.id.btnRemoveAttachment); - - ImageButton hamburger = view.findViewById(R.id.btnHamburger); - hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START)); // Set editor action listener for message field to send when enter is pressed - etMessage.setOnEditorActionListener((v, actionId, event) -> { + binding.etMessage.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) { - btnSend.performClick(); + binding.btnSend.performClick(); return true; } return false; }); //When the send button is clicked check if there is an attachment and send using the correct helper function - btnSend.setOnClickListener(v -> { + binding.btnSend.setOnClickListener(v -> { if (pendingAttachmentUri != null) { sendWithAttachment(pendingAttachmentUri); } else { @@ -152,13 +122,13 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis }); //When the attachment button is clicked open the file picker - btnAttach.setOnClickListener(v -> selectAttachment()); - btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); + binding.btnAttach.setOnClickListener(v -> selectAttachment()); + binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); setupRecyclerViews(); loadInitialData(); - return view; + return binding.getRoot(); } /** @@ -167,15 +137,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private void setupRecyclerViews() { // Set up Drawer menu to select conversation chatAdapter = new ChatAdapter(chatList, this); - rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); - rvChatList.setAdapter(chatAdapter); + binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvChatList.setAdapter(chatAdapter); // set up RecyclerView for selected chat to show messages messageAdapter = new MessageAdapter(messageList, null); LinearLayoutManager lm = new LinearLayoutManager(getContext()); lm.setStackFromEnd(true); - rvMessages.setLayoutManager(lm); - rvMessages.setAdapter(messageAdapter); + binding.rvMessages.setLayoutManager(lm); + binding.rvMessages.setAdapter(messageAdapter); setConversationActive(false); } @@ -247,7 +217,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis // Update title to customer name of active conversation for (Chat chat : chatList) { if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - tvChatTitle.setText(chat.getCustomerName()); + binding.tvChatTitle.setText(chat.getCustomerName()); break; } } @@ -270,8 +240,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis public void onChatClick(Chat chat) { activeConversationId = Long.parseLong(chat.getChatId()); setConversationActive(true); - tvChatTitle.setText(chat.getCustomerName()); - drawerLayout.closeDrawer(GravityCompat.START); + binding.tvChatTitle.setText(chat.getCustomerName()); + binding.chatDrawerLayout.closeDrawer(GravityCompat.START); if (stompChatManager != null) { stompChatManager.subscribeToConversation(activeConversationId); @@ -302,11 +272,11 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (activeConversationId == null) return; //get the message from text field - String text = etMessage.getText().toString().trim(); + String text = binding.etMessage.getText().toString().trim(); if (text.isEmpty()) return; //clear text field after sending - etMessage.setText(""); + binding.etMessage.setText(""); //calls api to send the message messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) @@ -332,18 +302,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis */ private void showAttachmentPreview(Uri uri) { pendingAttachmentUri = uri; - layoutAttachmentPreview.setVisibility(View.VISIBLE); + binding.layoutAttachmentPreview.setVisibility(View.VISIBLE); String mimeType = requireContext().getContentResolver().getType(uri); String fileName = getFileName(uri); - tvPreviewName.setText(fileName); + binding.tvPreviewName.setText(fileName); // If the file is an image, display a thumbnail of the image as well if (mimeType != null && mimeType.startsWith("image/")) { - ivPreview.setVisibility(View.VISIBLE); - Glide.with(this).load(uri).into(ivPreview); + binding.ivPreview.setVisibility(View.VISIBLE); + Glide.with(this).load(uri).into(binding.ivPreview); } else { - ivPreview.setVisibility(View.GONE); + binding.ivPreview.setVisibility(View.GONE); } } @@ -352,7 +322,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis */ private void removeAttachment() { pendingAttachmentUri = null; - layoutAttachmentPreview.setVisibility(View.GONE); + binding.layoutAttachmentPreview.setVisibility(View.GONE); } /** @@ -448,7 +418,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (activeConversationId != null && activeConversationId.equals(dto.getId())) { setConversationActive(true); - tvChatTitle.setText(name); + binding.tvChatTitle.setText(name); } } @@ -513,8 +483,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis */ private void scrollToBottom() { if (!messageList.isEmpty()) { - rvMessages.post(() -> - rvMessages.smoothScrollToPosition(messageList.size() - 1)); + binding.rvMessages.post(() -> + binding.rvMessages.smoothScrollToPosition(messageList.size() - 1)); } } @@ -546,23 +516,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis * Toggles the UI state based on whether a conversation is currently selected. */ private void setConversationActive(boolean active) { - btnSend.setEnabled(active); - etMessage.setEnabled(active); - btnAttach.setEnabled(active); + binding.btnSend.setEnabled(active); + binding.etMessage.setEnabled(active); + binding.btnAttach.setEnabled(active); if (!active) { activeConversationId = null; ChatNotificationService.activeConversationIdInUi = null; removeAttachment(); - if (tvChatTitle != null) tvChatTitle.setText("Customer Chat"); + if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat"); if (stompChatManager != null) { stompChatManager.clearConversationSubscription(); } messageList.clear(); messageAdapter.notifyDataSetChanged(); - etMessage.setText(""); - etMessage.setHint("Select a chat to start messaging"); + binding.etMessage.setText(""); + binding.etMessage.setHint("Select a chat to start messaging"); } else { - etMessage.setHint("Type a message..."); + binding.etMessage.setHint("Type a message..."); ChatNotificationService.activeConversationIdInUi = activeConversationId; } } @@ -573,6 +543,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis @Override public void onDestroyView() { super.onDestroyView(); + binding = null; ChatNotificationService.activeConversationIdInUi = null; if (stompChatManager != null) stompChatManager.disconnect(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index 5a2dcfc6..f2621091 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -14,11 +14,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import com.example.petstoremobile.R; - import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentListBinding; import javax.inject.Inject; @@ -28,12 +27,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ListFragment extends Fragment { - private DrawerLayout drawerLayout; - private LinearLayout drawerPets, drawerServices, drawerSuppliers; - private View touchBlocker; - - private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale; - + private FragmentListBinding binding; private NavController innerNavController; @Inject TokenManager tokenManager; @@ -42,49 +36,33 @@ public class ListFragment extends Fragment { * Inflates the fragment layout, initializes navigation drawers, and applies role-based access control. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_list, container, false); - - //get controls from the layout - drawerLayout = view.findViewById(R.id.drawerLayout); - drawerPets = view.findViewById(R.id.drawerPets); - drawerServices = view.findViewById(R.id.drawerServices); - drawerSuppliers = view.findViewById(R.id.drawerSuppliers); - drawerAdoptions = view.findViewById(R.id.drawerAdoptions); - drawerAppointments = view.findViewById(R.id.drawerAppointments); - drawerInventory = view.findViewById(R.id.drawerInventory); - drawerProducts = view.findViewById(R.id.drawerProducts); - drawerProductSupplier=view.findViewById(R.id.drawerProductSupplier); - drawerSale=view.findViewById(R.id.drawerSale); - drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView); + binding = FragmentListBinding.inflate(inflater, container, false); // Check user role and restrict access for STAFF String role = tokenManager.getRole(); if ("STAFF".equalsIgnoreCase(role)) { - drawerSuppliers.setVisibility(View.GONE); - drawerInventory.setVisibility(View.GONE); + binding.drawerSuppliers.setVisibility(View.GONE); + binding.drawerInventory.setVisibility(View.GONE); } - //needed to disable touches on the innerContainer while the drawer is open - touchBlocker = view.findViewById(R.id.touchBlocker); - //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments) //while the drawer is open - drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { + binding.drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { //When the drawer is opened, disable touches on the background @Override public void onDrawerOpened(View drawerView) { - touchBlocker.setVisibility(View.VISIBLE); - touchBlocker.setClickable(true); + binding.touchBlocker.setVisibility(View.VISIBLE); + binding.touchBlocker.setClickable(true); } //When the drawer is closed, enable touches again @Override public void onDrawerClosed(View drawerView) { - touchBlocker.setVisibility(View.GONE); - touchBlocker.setClickable(false); + binding.touchBlocker.setVisibility(View.GONE); + binding.touchBlocker.setClickable(false); } //unused methods @@ -95,18 +73,24 @@ public class ListFragment extends Fragment { }); // Click listeners for each drawer - drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet)); - drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service)); - drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier)); - drawerAdoptions.setOnClickListener(v -> navigateTo(R.id.nav_adoption)); - drawerAppointments.setOnClickListener(v -> navigateTo(R.id.nav_appointment)); - drawerInventory.setOnClickListener(v -> navigateTo(R.id.nav_inventory)); - drawerProducts.setOnClickListener(v -> navigateTo(R.id.nav_product)); - drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier)); - drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); - drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); + binding.drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet)); + binding.drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service)); + binding.drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier)); + binding.drawerAdoptions.setOnClickListener(v -> navigateTo(R.id.nav_adoption)); + binding.drawerAppointments.setOnClickListener(v -> navigateTo(R.id.nav_appointment)); + binding.drawerInventory.setOnClickListener(v -> navigateTo(R.id.nav_inventory)); + binding.drawerProducts.setOnClickListener(v -> navigateTo(R.id.nav_product)); + binding.drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier)); + binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order)); + binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale)); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -129,13 +113,13 @@ public class ListFragment extends Fragment { if (innerNavController != null) { innerNavController.navigate(destinationId); } - drawerLayout.closeDrawers(); + binding.drawerLayout.closeDrawers(); } /** * Programmatically opens the navigation drawer. */ public void openDrawer() { - drawerLayout.openDrawer(GravityCompat.START); + binding.drawerLayout.openDrawer(GravityCompat.START); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index bcc8a0d8..9d8a472f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments; import android.net.Uri; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; @@ -10,19 +11,16 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.activities.MainActivity; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentProfileBinding; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.services.ChatNotificationService; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; @@ -41,9 +39,6 @@ import dagger.hilt.android.AndroidEntryPoint; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; /** * Fragment that displays and allows editing of the user's profile information. @@ -51,9 +46,7 @@ import retrofit2.Response; @AndroidEntryPoint public class ProfileFragment extends Fragment { - //initialize the view/controls - private ImageView imgProfile; - private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole; + private FragmentProfileBinding binding; private UserDTO currentUser; private boolean hasImage = false; @@ -87,37 +80,26 @@ public class ProfileFragment extends Fragment { * Inflates the fragment layout and sets up listeners for profile. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_profile, container, false); - - //get all the controls from the view - imgProfile = view.findViewById(R.id.imgProfile); - tvProfileName = view.findViewById(R.id.tvProfileName); - tvProfileEmail = view.findViewById(R.id.tvProfileEmail); - tvProfilePhone = view.findViewById(R.id.tvProfilePhone); - tvProfileRole = view.findViewById(R.id.tvProfileRole); - Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto); - Button btnEditEmail = view.findViewById(R.id.btnEditEmail); - Button btnEditPhone = view.findViewById(R.id.btnEditPhone); - Button btnLogout = view.findViewById(R.id.btnLogout); + binding = FragmentProfileBinding.inflate(inflater, container, false); //Load Profile Data from backend loadProfileData(); //Set up listeners for the buttons //Change photo button - btnChangePhoto.setOnClickListener(v -> { + binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage); }); //Edit email button //When clicked open a dialog to change email - btnEditEmail.setOnClickListener(v -> { + binding.btnEditEmail.setOnClickListener(v -> { //Make a text field for the user to enter the new email EditText input = new EditText(requireContext()); input.setPadding(30,30,30,30); - input.setText(tvProfileEmail.getText().toString()); + input.setText(binding.tvProfileEmail.getText().toString()); //set input type to email input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); @@ -140,11 +122,11 @@ public class ProfileFragment extends Fragment { //Edit phone button //When clicked open a dialog to change phone - btnEditPhone.setOnClickListener(v -> { + binding.btnEditPhone.setOnClickListener(v -> { //Make a text field for the user to enter the new email EditText input = new EditText(requireContext()); input.setPadding(30,30,30,30); - input.setText(tvProfilePhone.getText().toString()); + input.setText(binding.tvProfilePhone.getText().toString()); //set input type to phone number input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE); @@ -169,7 +151,7 @@ public class ProfileFragment extends Fragment { }); //Logout button - btnLogout.setOnClickListener(v -> { + binding.btnLogout.setOnClickListener(v -> { // Stop notification service before logging out so notifications stop android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class); requireContext().stopService(serviceIntent); @@ -183,7 +165,13 @@ public class ProfileFragment extends Fragment { requireActivity().finish(); }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -194,16 +182,16 @@ public class ProfileFragment extends Fragment { currentUser = result; //set the user data to the view - tvProfileName.setText(currentUser.getFullName()); - tvProfileEmail.setText(currentUser.getEmail()); - tvProfilePhone.setText(currentUser.getPhone()); - tvProfileRole.setText(currentUser.getRole()); + binding.tvProfileName.setText(currentUser.getFullName()); + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); + binding.tvProfileRole.setText(currentUser.getRole()); // get the avatar endpoint to load profile image and the token for authorization String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { @Override public void onResourceReady() { hasImage = true; @@ -246,7 +234,7 @@ public class ProfileFragment extends Fragment { private void deleteAvatar() { authApi.deleteAvatar().enqueue(RetrofitUtils.createCallback(requireContext(), "DELETE_AVATAR", "Avatar removed successfully", result -> { hasImage = false; - imgProfile.setImageResource(R.drawable.placeholder); + binding.imgProfile.setImageResource(R.drawable.placeholder); })); } @@ -260,8 +248,8 @@ public class ProfileFragment extends Fragment { authApi.updateMe(updates).enqueue(RetrofitUtils.createCallback(requireContext(), "UPDATE_PROFILE", "Profile updated successfully", result -> { currentUser = result; // Update the view with the new data from backend - tvProfileEmail.setText(currentUser.getEmail()); - tvProfilePhone.setText(currentUser.getPhone()); + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); })); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index cae5fd2f..04713d86 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.graphics.Color; import android.os.Bundle; -import android.text.*; import android.util.Log; import android.view.*; import android.widget.*; @@ -12,20 +11,17 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AdoptionAdapter; +import com.example.petstoremobile.databinding.FragmentAdoptionBinding; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.AdoptionViewModel; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; import com.prolificinteractive.materialcalendarview.MaterialCalendarView; -import com.prolificinteractive.materialcalendarview.OnDateSelectedListener; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; @@ -35,15 +31,11 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { + private FragmentAdoptionBinding binding; private List adoptionList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private AdoptionAdapter adapter; private AdoptionViewModel viewModel; - private SwipeRefreshLayout swipeRefresh; - private EditText etSearch; - private ImageButton hamburger; - private ImageButton btnToggleCalendarMode; - private MaterialCalendarView calendarView; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @@ -63,22 +55,17 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_adoption, container, false); + binding = FragmentAdoptionBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburgerAdoption); - calendarView = view.findViewById(R.id.calendarViewAdoption); - btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarModeAdoption); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); setupCalendar(); loadAdoptions(); - FloatingActionButton fab = view.findViewById(R.id.fabAddAdoption); - fab.setOnClickListener(v -> openDetail(-1)); + binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1)); - hamburger.setOnClickListener(v -> { + binding.btnHamburgerAdoption.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -88,9 +75,15 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop } }); - btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); + binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -98,7 +91,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; - calendarView.state().edit() + binding.calendarViewAdoption.state().edit() .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) .commit(); } @@ -107,21 +100,18 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop * Sets up the date selection listener for the calendar. */ private void setupCalendar() { - calendarView.setOnDateChangedListener(new OnDateSelectedListener() { - @Override - public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) { - if (selected) { - if (date.equals(selectedCalendarDay)) { - selectedCalendarDay = null; - calendarView.clearSelection(); - } else { - selectedCalendarDay = date; - } - } else { + binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> { + if (selected) { + if (date.equals(selectedCalendarDay)) { selectedCalendarDay = null; + binding.calendarViewAdoption.clearSelection(); + } else { + selectedCalendarDay = date; } - filter(etSearch.getText().toString()); + } else { + selectedCalendarDay = null; } + filter(binding.etSearchAdoption.getText().toString()); }); } @@ -144,28 +134,26 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate()); } } - calendarView.removeDecorators(); - calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); + binding.calendarViewAdoption.removeDecorators(); + binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions)); } /** * Initializes the RecyclerView for displaying adoptions. */ - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewAdoptions); + private void setupRecyclerView() { adapter = new AdoptionAdapter(filteredList, this); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewAdoptions.setAdapter(adapter); } /** * Sets up the search bar for filtering */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchAdoption); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchAdoption.addTextChangedListener(new android.text.TextWatcher() { public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(Editable s) {} + public void afterTextChanged(android.text.Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { filter(s.toString()); } @@ -175,9 +163,8 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop /** * Sets up the SwipeRefreshLayout to reload adoption data. */ - private void setupSwipeRefresh(View view) { - swipeRefresh = view.findViewById(R.id.swipeRefreshAdoption); - swipeRefresh.setOnRefreshListener(this::loadAdoptions); + private void setupSwipeRefresh() { + binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions); } /** @@ -221,21 +208,21 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + binding.swipeRefreshAdoption.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshAdoption.setRefreshing(false); if (resource.data != null) { adoptionList.clear(); adoptionList.addAll(resource.data.getContent()); updateCalendarDecorators(); - filter(etSearch != null ? etSearch.getText().toString() : ""); + filter(binding.etSearchAdoption != null ? binding.etSearchAdoption.getText().toString() : ""); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshAdoption.setRefreshing(false); Toast.makeText(getContext(), "Failed to load adoptions: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("AdoptionFragment", "Error loading adoptions: " + resource.message); break; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index f62475c0..fb1455ae 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -9,20 +9,18 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; +import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.dtos.PetDTO; @@ -32,11 +30,8 @@ import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.ServiceViewModel; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; -import com.prolificinteractive.materialcalendarview.MaterialCalendarView; -import com.prolificinteractive.materialcalendarview.OnDateSelectedListener; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -52,6 +47,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { + private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private List petList = new ArrayList<>(); @@ -62,11 +58,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private PetViewModel petViewModel; private ServiceViewModel serviceViewModel; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; - private ImageButton hamburger; - private ImageButton btnToggleCalendarMode; - private MaterialCalendarView calendarView; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); @@ -88,24 +79,19 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_appointment, container, false); + binding = FragmentAppointmentBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburger); - calendarView = view.findViewById(R.id.calendarView); - btnToggleCalendarMode = view.findViewById(R.id.btnToggleCalendarMode); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); setupCalendar(); loadAppointmentData(); loadPets(); loadServices(); - FloatingActionButton fabAdd = view.findViewById(R.id.fabAddAppointment); - fabAdd.setOnClickListener(v -> openAppointmentDetails(-1)); + binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); - hamburger.setOnClickListener(v -> { + binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -115,9 +101,15 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } }); - btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); + binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -125,7 +117,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void toggleCalendarMode() { isMonthMode = !isMonthMode; - calendarView.state().edit() + binding.calendarView.state().edit() .setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS) .commit(); } @@ -134,21 +126,18 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. * Sets up the date selection listener for the calendar. */ private void setupCalendar() { - calendarView.setOnDateChangedListener(new OnDateSelectedListener() { - @Override - public void onDateSelected(@NonNull MaterialCalendarView widget, @NonNull CalendarDay date, boolean selected) { - if (selected) { - if (date.equals(selectedCalendarDay)) { - selectedCalendarDay = null; - calendarView.clearSelection(); - } else { - selectedCalendarDay = date; - } - } else { + binding.calendarView.setOnDateChangedListener((widget, date, selected) -> { + if (selected) { + if (date.equals(selectedCalendarDay)) { selectedCalendarDay = null; + binding.calendarView.clearSelection(); + } else { + selectedCalendarDay = date; } - filterAppointments(etSearch.getText().toString()); + } else { + selectedCalendarDay = null; } + filterAppointments(binding.etSearchAppointment.getText().toString()); }); } @@ -172,16 +161,15 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } } //update the indicators to the calendar - calendarView.removeDecorators(); - calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); + binding.calendarView.removeDecorators(); + binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments)); } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchAppointment); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchAppointment.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @@ -229,9 +217,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. /** * Initializes the SwipeRefreshLayout to allow manual data refreshing. */ - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment); - swipeRefreshLayout.setOnRefreshListener(this::loadAppointmentData); + private void setupSwipeRefresh() { + binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData); } /** @@ -290,21 +277,21 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + binding.swipeRefreshAppointment.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshAppointment.setRefreshing(false); if (resource.data != null) { appointmentList.clear(); appointmentList.addAll(resource.data.getContent()); updateCalendarDecorators(); - filterAppointments(etSearch != null ? etSearch.getText().toString() : ""); + filterAppointments(binding.etSearchAppointment != null ? binding.etSearchAppointment.getText().toString() : ""); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshAppointment.setRefreshing(false); Toast.makeText(getContext(), "Failed to load appointments: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("AppointmentFragment", "Error loading appointments: " + resource.message); break; @@ -336,34 +323,12 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Get a pet's name based on its ID. - */ - private String getPetName(Long id) { - for (PetDTO p : petList) { - if (p.getPetId().equals(id)) return p.getPetName(); - - } - return ""; - } - - /** - * Get a service's name based on its ID. - */ - private String getServiceName(Long id) { - for (ServiceDTO s : serviceList) { - if (s.getServiceId().equals(id))return s.getServiceName(); - } - return ""; - } - /** * Initializes the RecyclerView for displaying appointments. */ - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments); + private void setupRecyclerView() { adapter = new AppointmentAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); + binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewAppointments.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 799fe39f..07e08aba 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -10,11 +10,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.Spinner; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -24,11 +19,11 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; +import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.fragments.ListFragment; @@ -46,18 +41,12 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private static final String TAG = "InventoryFragment"; private static final int PAGE_SIZE = 20; + private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); private final List categoryList = new ArrayList<>(); private InventoryAdapter adapter; private InventoryViewModel viewModel; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; - private Spinner spinnerCategory; - private ImageButton hamburger; - private Button btnBulkDelete; - private TextView tvSelectionCount; - // Debounce search private final Handler searchHandler = new Handler(Looper.getMainLooper()); private Runnable searchRunnable; @@ -89,23 +78,17 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_inventory, container, false); + binding = FragmentInventoryBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburger); - btnBulkDelete = view.findViewById(R.id.btnBulkDelete); - tvSelectionCount = view.findViewById(R.id.tvSelectionCount); - spinnerCategory = view.findViewById(R.id.spinnerCategory); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadCategories(); // loads categories then triggers loadInventory loadInventory(true); - view.findViewById(R.id.fabAddInventory) - .setOnClickListener(v -> openDetail(null)); + binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); - hamburger.setOnClickListener(v -> { + binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -115,9 +98,15 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } }); - btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); + binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -153,9 +142,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn android.R.layout.simple_spinner_item, categoryNames); spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerCategory.setAdapter(spinnerAdapter); + binding.spinnerCategory.setAdapter(spinnerAdapter); - spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (!spinnerReady) { @@ -181,15 +170,12 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn /** * Sets up the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchInventory); - etSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + private void setupSearch() { + binding.etSearchInventory.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } - @Override - public void afterTextChanged(Editable s) { + @Override public void afterTextChanged(Editable s) { } @Override @@ -208,18 +194,17 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn /** * Initializes the RecyclerView with a layout manager, and adapter. */ - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewInventory); + private void setupRecyclerView() { adapter = new InventoryAdapter(inventoryList, this); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewInventory.setAdapter(adapter); - rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (dy <= 0) return; - LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); + LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.getLayoutManager(); if (lm == null) return; int visible = lm.getChildCount(); @@ -235,9 +220,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn /** * Sets up the SwipeRefreshLayout to reload the first page of inventory items. */ - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); - swipeRefreshLayout.setOnRefreshListener(() -> loadInventory(true)); + private void setupSwipeRefresh() { + binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true)); } /** @@ -264,12 +248,12 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn case LOADING: // Show loading indicator isLoading = true; - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + binding.swipeRefreshInventory.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data isLoading = false; - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshInventory.setRefreshing(false); if (resource.data != null) { if (reset) inventoryList.clear(); inventoryList.addAll(resource.data.getContent()); @@ -281,7 +265,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn case ERROR: // Hide loading indicator and toast error message isLoading = false; - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshInventory.setRefreshing(false); Log.e(TAG, "Error: " + resource.message); Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); break; @@ -343,10 +327,10 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn * Hides the bulk deletion UI bar. */ private void hideBulkDeleteBar() { - if (btnBulkDelete != null) - btnBulkDelete.setVisibility(View.GONE); - if (tvSelectionCount != null) - tvSelectionCount.setVisibility(View.GONE); + if (binding != null) { + binding.btnBulkDelete.setVisibility(View.GONE); + binding.tvSelectionCount.setVisibility(View.GONE); + } } /** @@ -389,9 +373,9 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn @Override public void onSelectionChanged(int selectedCount) { if (selectedCount > 0) { - btnBulkDelete.setVisibility(View.VISIBLE); - tvSelectionCount.setVisibility(View.VISIBLE); - tvSelectionCount.setText(selectedCount + " selected"); + binding.btnBulkDelete.setVisibility(View.VISIBLE); + binding.tvSelectionCount.setVisibility(View.VISIBLE); + binding.tvSelectionCount.setText(selectedCount + " selected"); } else { hideBulkDeleteBar(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 44198344..0c71593a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -2,13 +2,12 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; @@ -17,18 +16,15 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.EditText; -import android.widget.ImageButton; -import android.widget.Spinner; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; +import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.PetViewModel; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; @@ -40,16 +36,13 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { + private FragmentPetBinding binding; private List petList = new ArrayList<>(); private List filteredList = new ArrayList<>(); - private ImageButton hamburger; private PetAdapter adapter; private PetViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; - private Spinner spinnerStatus; /** * Initializes the fragment and its associated PetViewModel. @@ -64,21 +57,18 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Sets up the fragment's UI components, including RecyclerView, search, status filter, and swipe-to-refresh. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_pet, container, false); + binding = FragmentPetBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburger); + setupRecyclerView(); + setupSearch(); + setupStatusFilter(); + setupSwipeRefresh(); - setupRecyclerView(view); - setupSearch(view); - setupStatusFilter(view); - setupSwipeRefresh(view); + binding.fabAddPet.setOnClickListener(v -> openPetDetails()); - FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet); - fabAddPet.setOnClickListener(v -> openPetDetails()); - - hamburger.setOnClickListener(v -> { + binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -88,7 +78,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -103,9 +99,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen /** * Configures the search bar with a for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchPet); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchPet.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { filterPets(); @@ -117,14 +112,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen /** * Configures the status filter spinner. */ - private void setupStatusFilter(View view) { - spinnerStatus = view.findViewById(R.id.spinnerStatus); + private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted"}; BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerStatus.setAdapter(adapter); + binding.spinnerStatus.setAdapter(adapter); - spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { filterPets(); @@ -139,8 +133,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Filters the pet list based on both the search query and the selected status. */ private void filterPets() { - String query = etSearch.getText().toString().toLowerCase(); - String selectedStatus = spinnerStatus.getSelectedItem().toString(); + String query = binding.etSearchPet.getText().toString().toLowerCase(); + String selectedStatus = binding.spinnerStatus.getSelectedItem().toString(); filteredList.clear(); for (PetDTO p : petList) { @@ -162,9 +156,8 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen /** * Sets up the SwipeRefreshLayout to allow manual re-fetching of pet data. */ - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet); - swipeRefreshLayout.setOnRefreshListener(this::loadPetData); + private void setupSwipeRefresh() { + binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); } /** @@ -208,19 +201,15 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Fetches all pet data from the server via the ViewModel and updates the UI. */ private void loadPetData() { - //Load all pets from the backend using viewModel viewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; - // Check the status to see if the resource is loaded and display the data switch (resource.status) { case LOADING: - // Show loading indicator - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + binding.swipeRefreshPet.setRefreshing(true); break; case SUCCESS: - // Hide loading indicator and display data - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshPet.setRefreshing(false); if (resource.data != null) { petList.clear(); petList.addAll(resource.data.getContent()); @@ -228,8 +217,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } break; case ERROR: - // Hide loading indicator and toast error message - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshPet.setRefreshing(false); Toast.makeText(getContext(), "Failed to load pets: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("PetFragment", "Error loading pets: " + resource.message); break; @@ -240,11 +228,10 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen /** * Initializes the RecyclerView with a layout manager and adapter for displaying pets. */ - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); + private void setupRecyclerView() { adapter = new PetAdapter(filteredList, this); adapter.setBaseUrl(baseUrl); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); + binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPets.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 61adfcf5..a459262f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -11,16 +11,15 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentProductBinding; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.ProductViewModel; -import com.example.petstoremobile.utils.Resource; -import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.*; import javax.inject.Inject; @@ -31,11 +30,10 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { + private FragmentProductBinding binding; private List productList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private ProductAdapter adapter; - private SwipeRefreshLayout swipeRefresh; - private EditText etSearch; private ProductViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; @@ -56,19 +54,17 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_product, container, false); + binding = FragmentProductBinding.inflate(inflater, container, false); - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadProducts(); - FloatingActionButton fab = view.findViewById(R.id.fabAddProduct); - fab.setOnClickListener(v -> openDetail(-1)); + binding.fabAddProduct.setOnClickListener(v -> openDetail(-1)); - ImageButton hamburger = view.findViewById(R.id.btnHamburgerProduct); - hamburger.setOnClickListener(v -> { + binding.btnHamburgerProduct.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -78,27 +74,31 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Initializes the RecyclerView with a layout manager and adapter for displaying products. */ - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewProducts); + private void setupRecyclerView() { adapter = new ProductAdapter(filteredList, this); adapter.setBaseUrl(baseUrl); adapter.setToken(tokenManager.getToken()); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchProduct); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchProduct.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int a, int b, int c) {} public void afterTextChanged(Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { @@ -110,16 +110,15 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc /** * Sets up the SwipeRefreshLayout to allow manual re-fetching of product data. */ - private void setupSwipeRefresh(View view) { - swipeRefresh = view.findViewById(R.id.swipeRefreshProduct); - swipeRefresh.setOnRefreshListener(this::loadProducts); + private void setupSwipeRefresh() { + binding.swipeRefreshProduct.setOnRefreshListener(this::loadProducts); } /** * Filters the product list based on the search query across name, category, and description. */ private void filter() { - String query = etSearch.getText().toString().toLowerCase(); + String query = binding.etSearchProduct.getText().toString().toLowerCase(); filteredList.clear(); for (ProductDTO p : productList) { @@ -139,19 +138,15 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc * Fetches all product data from the server through the ViewModel and updates the UI. */ private void loadProducts() { - //Load all products from the backend using viewModel viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; - // Check the status to see if the resource is loaded and display the data switch (resource.status) { case LOADING: - // Show loading indicator - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + binding.swipeRefreshProduct.setRefreshing(true); break; case SUCCESS: - // Hide loading indicator and display data - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshProduct.setRefreshing(false); if (resource.data != null) { productList.clear(); productList.addAll(resource.data.getContent()); @@ -159,8 +154,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } break; case ERROR: - // Hide loading indicator and toast error message - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshProduct.setRefreshing(false); Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("ProductFragment", "Error loading products: " + resource.message); break; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index d77331ca..49eccc94 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -11,15 +11,14 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; +import com.example.petstoremobile.databinding.FragmentProductSupplierBinding; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; -import com.example.petstoremobile.utils.Resource; -import com.google.android.material.floatingactionbutton.FloatingActionButton; + import java.util.*; import dagger.hilt.android.AndroidEntryPoint; @@ -28,11 +27,10 @@ import dagger.hilt.android.AndroidEntryPoint; public class ProductSupplierFragment extends Fragment implements ProductSupplierAdapter.OnProductSupplierClickListener { + private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private ProductSupplierAdapter adapter; - private SwipeRefreshLayout swipeRefresh; - private EditText etSearch; private ProductSupplierViewModel viewModel; /** @@ -50,18 +48,16 @@ public class ProductSupplierFragment extends Fragment @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_product_supplier, container, false); + binding = FragmentProductSupplierBinding.inflate(inflater, container, false); - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadData(); - FloatingActionButton fab = view.findViewById(R.id.fabAddPS); - fab.setOnClickListener(v -> openDetail(-1)); + binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); - ImageButton hamburger = view.findViewById(R.id.btnHamburgerPS); - hamburger.setOnClickListener(v -> { + binding.btnHamburgerPS.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -71,25 +67,29 @@ public class ProductSupplierFragment extends Fragment } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. */ - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewPS); + private void setupRecyclerView() { adapter = new ProductSupplierAdapter(filteredList, this); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPS.setAdapter(adapter); } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchPS); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchPS.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int a, int b, int c) {} public void afterTextChanged(Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { @@ -101,9 +101,8 @@ public class ProductSupplierFragment extends Fragment /** * Sets up the SwipeRefreshLayout to allow manual reloading of product-supplier data. */ - private void setupSwipeRefresh(View view) { - swipeRefresh = view.findViewById(R.id.swipeRefreshPS); - swipeRefresh.setOnRefreshListener(this::loadData); + private void setupSwipeRefresh() { + binding.swipeRefreshPS.setOnRefreshListener(this::loadData); } /** @@ -137,20 +136,20 @@ public class ProductSupplierFragment extends Fragment switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + binding.swipeRefreshPS.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshPS.setRefreshing(false); if (resource.data != null) { psList.clear(); psList.addAll(resource.data.getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); + filter(binding.etSearchPS != null ? binding.etSearchPS.getText().toString() : ""); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshPS.setRefreshing(false); Toast.makeText(getContext(), "Failed to load: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("PSFragment", "Error loading: " + resource.message); break; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 36050661..078ef06c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -11,14 +11,14 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PurchaseOrderAdapter; +import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; -import com.example.petstoremobile.utils.Resource; + import java.util.*; import dagger.hilt.android.AndroidEntryPoint; @@ -27,11 +27,10 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderFragment extends Fragment implements PurchaseOrderAdapter.OnPurchaseOrderClickListener { + private FragmentPurchaseOrderBinding binding; private List poList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private PurchaseOrderAdapter adapter; - private SwipeRefreshLayout swipeRefresh; - private EditText etSearch; private PurchaseOrderViewModel viewModel; /** @@ -47,17 +46,16 @@ public class PurchaseOrderFragment extends Fragment * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_purchase_order, container, false); + binding = FragmentPurchaseOrderBinding.inflate(inflater, container, false); - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadData(); - ImageButton hamburger = view.findViewById(R.id.btnHamburgerPO); - hamburger.setOnClickListener(v -> { + binding.btnHamburgerPO.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -67,25 +65,29 @@ public class PurchaseOrderFragment extends Fragment } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Initializes the RecyclerView with a layout manager and adapter for purchase orders. */ - private void setupRecyclerView(View view) { - RecyclerView rv = view.findViewById(R.id.recyclerViewPO); + private void setupRecyclerView() { adapter = new PurchaseOrderAdapter(filteredList, this); - rv.setLayoutManager(new LinearLayoutManager(getContext())); - rv.setAdapter(adapter); + binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPO.setAdapter(adapter); } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchPO); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchPO.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int a, int b, int c) { } @@ -101,9 +103,8 @@ public class PurchaseOrderFragment extends Fragment /** * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. */ - private void setupSwipeRefresh(View view) { - swipeRefresh = view.findViewById(R.id.swipeRefreshPO); - swipeRefresh.setOnRefreshListener(this::loadData); + private void setupSwipeRefresh() { + binding.swipeRefreshPO.setOnRefreshListener(this::loadData); } /** @@ -137,20 +138,20 @@ public class PurchaseOrderFragment extends Fragment switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + binding.swipeRefreshPO.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshPO.setRefreshing(false); if (resource.data != null) { poList.clear(); poList.addAll(resource.data.getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); + filter(binding.etSearchPO != null ? binding.etSearchPO.getText().toString() : ""); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + binding.swipeRefreshPO.setRefreshing(false); Toast.makeText(getContext(), "Failed to load purchase orders: " + resource.message, Toast.LENGTH_SHORT).show(); Log.e("POFragment", "Error loading purchase orders: " + resource.message); break; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 859bacca..99f249db 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -2,23 +2,19 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; import com.example.petstoremobile.api.SaleApi; +import com.example.petstoremobile.databinding.FragmentSaleBinding; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.models.Sale; import java.util.ArrayList; @@ -31,46 +27,45 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { + private FragmentSaleBinding binding; private List saleList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private SaleAdapter adapter; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; - private ImageButton btnHamburger; @Inject SaleApi api; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_sale, container, false); + binding = FragmentSaleBinding.inflate(inflater, container, false); - btnHamburger = view.findViewById(R.id.btnHamburger); - - setupRecyclerView(view); + setupRecyclerView(); loadSaleData(); - setupSearch(view); - setupSwipeRefresh(view); + setupSearch(); + setupSwipeRefresh(); // Make the hamburger button open the drawer from listFragment - if (btnHamburger != null) { - btnHamburger.setOnClickListener(v -> { - Fragment parent = getParentFragment(); - if (parent != null) { - Fragment grandParent = parent.getParentFragment(); - if (grandParent instanceof ListFragment) { - ((ListFragment) grandParent).openDrawer(); - } + binding.btnHamburger.setOnClickListener(v -> { + Fragment parent = getParentFragment(); + if (parent != null) { + Fragment grandParent = parent.getParentFragment(); + if (grandParent instanceof ListFragment) { + ((ListFragment) grandParent).openDrawer(); } - }); - } + } + }); - return view; + return binding.getRoot(); } - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchSale); - etSearch.addTextChangedListener(new TextWatcher() { + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + private void setupSearch() { + binding.etSearchSale.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @@ -104,11 +99,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis adapter.notifyDataSetChanged(); } - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSale); - swipeRefreshLayout.setOnRefreshListener(() -> { + private void setupSwipeRefresh() { + binding.swipeRefreshSale.setOnRefreshListener(() -> { loadSaleData(); - swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshSale.setRefreshing(false); }); } @@ -143,10 +137,9 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis adapter.notifyDataSetChanged(); } - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSales); + private void setupRecyclerView() { adapter = new SaleAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); + binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewSales.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index ff0819b2..8ae3c5a5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -8,8 +8,6 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; @@ -17,17 +15,14 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ServiceAdapter; +import com.example.petstoremobile.databinding.FragmentServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.ServiceViewModel; -import com.example.petstoremobile.utils.Resource; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; @@ -37,13 +32,11 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { + private FragmentServiceBinding binding; private List serviceList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private ServiceAdapter adapter; - private ImageButton hamburger; private ServiceViewModel viewModel; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; /** * Initializes the fragment and its associated ServiceViewModel. @@ -60,21 +53,18 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_service, container, false); + binding = FragmentServiceBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburger); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadServiceData(); //Add button to opens the add dialog - FloatingActionButton fabAddService = view.findViewById(R.id.fabAddService); - fabAddService.setOnClickListener(v -> openServiceDetails(-1)); + binding.fabAddService.setOnClickListener(v -> openServiceDetails(-1)); //Make the hamburger button open the drawer from listFragment - hamburger.setOnClickListener(v -> { + binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -84,15 +74,20 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchService); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchService.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { filterServices(s.toString()); @@ -123,9 +118,8 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic /** * Sets up the SwipeRefreshLayout to allow manual reloading of service data. */ - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService); - swipeRefreshLayout.setOnRefreshListener(this::loadServiceData); + private void setupSwipeRefresh() { + binding.swipeRefreshService.setOnRefreshListener(this::loadServiceData); } /** @@ -169,20 +163,20 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + binding.swipeRefreshService.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshService.setRefreshing(false); if (resource.data != null) { serviceList.clear(); serviceList.addAll(resource.data.getContent()); - filterServices(etSearch != null ? etSearch.getText().toString() : ""); + filterServices(binding.etSearchService != null ? binding.etSearchService.getText().toString() : ""); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshService.setRefreshing(false); if (getContext() != null) { Toast.makeText(getContext(), "Failed to load services: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -195,10 +189,9 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic /** * Initializes the RecyclerView with a layout manager and adapter for services. */ - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices); + private void setupRecyclerView() { adapter = new ServiceAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); + binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewServices.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index baa67b5b..4018c27a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -8,8 +8,6 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.text.Editable; import android.text.TextWatcher; @@ -17,17 +15,14 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageButton; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SupplierAdapter; +import com.example.petstoremobile.databinding.FragmentSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.SupplierViewModel; -import com.example.petstoremobile.utils.Resource; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import java.util.ArrayList; import java.util.List; @@ -37,13 +32,11 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { + private FragmentSupplierBinding binding; private List supplierList = new ArrayList<>(); private List filteredList = new ArrayList<>(); private SupplierAdapter adapter; - private ImageButton hamburger; private SupplierViewModel viewModel; - private SwipeRefreshLayout swipeRefreshLayout; - private EditText etSearch; /** * Initializes the fragment and its associated SupplierViewModel. @@ -60,21 +53,18 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_supplier, container, false); + binding = FragmentSupplierBinding.inflate(inflater, container, false); - hamburger = view.findViewById(R.id.btnHamburger); - - setupRecyclerView(view); - setupSearch(view); - setupSwipeRefresh(view); + setupRecyclerView(); + setupSearch(); + setupSwipeRefresh(); loadSupplierData(); //Add button to opens the add dialog - FloatingActionButton fabAddSupplier = view.findViewById(R.id.fabAddSupplier); - fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); + binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); //Make the hamburger button open the drawer from listFragment - hamburger.setOnClickListener(v -> { + binding.btnHamburger.setOnClickListener(v -> { Fragment parent = getParentFragment(); if (parent != null) { Fragment grandParent = parent.getParentFragment(); @@ -84,15 +74,20 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp } }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Configures the search bar for filtering. */ - private void setupSearch(View view) { - etSearch = view.findViewById(R.id.etSearchSupplier); - etSearch.addTextChangedListener(new TextWatcher() { + private void setupSearch() { + binding.etSearchSupplier.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { filterSuppliers(s.toString()); @@ -124,9 +119,8 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp /** * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. */ - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier); - swipeRefreshLayout.setOnRefreshListener(this::loadSupplierData); + private void setupSwipeRefresh() { + binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData); } /** @@ -172,20 +166,20 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp switch (resource.status) { case LOADING: // Show loading indicator - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(true); + binding.swipeRefreshSupplier.setRefreshing(true); break; case SUCCESS: // Hide loading indicator and display data - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshSupplier.setRefreshing(false); if (resource.data != null) { supplierList.clear(); supplierList.addAll(resource.data.getContent()); - filterSuppliers(etSearch.getText().toString()); + filterSuppliers(binding.etSearchSupplier.getText().toString()); } break; case ERROR: // Hide loading indicator and toast error message - if (swipeRefreshLayout != null) swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshSupplier.setRefreshing(false); if (getContext() != null) { Toast.makeText(getContext(), "Failed to load suppliers: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -198,10 +192,9 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp /** * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. */ - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers); + private void setupRecyclerView() { adapter = new SupplierAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); + binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewSuppliers.setAdapter(adapter); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 2aa140bf..3702e168 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -2,7 +2,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.app.DatePickerDialog; import android.os.Bundle; -import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; @@ -10,7 +9,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentAdoptionDetailBinding; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; @@ -29,10 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class AdoptionDetailFragment extends Fragment { - private TextView tvMode, tvAdoptionId; - private EditText etAdoptionDate; - private Spinner spinnerPet, spinnerCustomer, spinnerStatus; - private Button btnSave, btnDelete, btnBack; + private FragmentAdoptionDetailBinding binding; private long adoptionId = -1; private boolean isEditing = false; @@ -59,49 +55,39 @@ public class AdoptionDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false); - initViews(view); + binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false); setupSpinners(); setupDatePicker(); loadData(); handleArguments(); - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> saveAdoption()); - btnDelete.setOnClickListener(v -> confirmDelete()); - return view; + binding.btnAdoptionBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption()); + binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); + return binding.getRoot(); } - /** - * Initializes UI components from the layout. - */ - private void initViews(View v) { - tvMode = v.findViewById(R.id.tvAdoptionMode); - tvAdoptionId = v.findViewById(R.id.tvAdoptionId); - etAdoptionDate = v.findViewById(R.id.etAdoptionDate); - spinnerPet = v.findViewById(R.id.spinnerAdoptionPet); - spinnerCustomer= v.findViewById(R.id.spinnerAdoptionCustomer); - spinnerStatus = v.findViewById(R.id.spinnerAdoptionStatus); - btnSave = v.findViewById(R.id.btnSaveAdoption); - btnDelete = v.findViewById(R.id.btnDeleteAdoption); - btnBack = v.findViewById(R.id.btnAdoptionBack); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Configures the spinner for adoption status. */ private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), spinnerStatus, STATUSES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAdoptionStatus, STATUSES); } /** * Configures the date picker dialog for the adoption date field. */ private void setupDatePicker() { - etAdoptionDate.setOnClickListener(v -> { + binding.etAdoptionDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); new DatePickerDialog(requireContext(), - (dp, y, m, d) -> etAdoptionDate.setText( + (dp, y, m, d) -> binding.etAdoptionDate.setText( String.format("%04d-%02d-%02d", y, m + 1, d)), c.get(Calendar.YEAR), c.get(Calendar.MONTH), @@ -124,7 +110,7 @@ public class AdoptionDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, PetDTO::getPetName, "-- Select Pet --", preselectedPetId, PetDTO::getPetId); } @@ -138,7 +124,7 @@ public class AdoptionDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, item -> item.getFirstName() + " " + item.getLastName(), "-- Select Customer --", preselectedCustomerId, CustomerDTO::getCustomerId); @@ -157,18 +143,18 @@ public class AdoptionDetailFragment extends Fragment { preselectedPetId = a.getLong("petId", -1); preselectedCustomerId = a.getLong("customerId", -1); - tvMode.setText("Edit Adoption"); - tvAdoptionId.setText("ID: " + adoptionId); - tvAdoptionId.setVisibility(View.VISIBLE); - etAdoptionDate.setText(a.getString("adoptionDate")); - btnDelete.setVisibility(View.VISIBLE); + binding.tvAdoptionMode.setText("Edit Adoption"); + binding.tvAdoptionId.setText("ID: " + adoptionId); + binding.tvAdoptionId.setVisibility(View.VISIBLE); + binding.etAdoptionDate.setText(a.getString("adoptionDate")); + binding.btnDeleteAdoption.setVisibility(View.VISIBLE); // Pre-fill status - SpinnerUtils.setSelectionByValue(spinnerStatus, a.getString("adoptionStatus", "Pending")); + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getString("adoptionStatus", "Pending")); } else { - tvMode.setText("Add Adoption"); - btnDelete.setVisibility(View.GONE); - tvAdoptionId.setVisibility(View.GONE); + binding.tvAdoptionMode.setText("Add Adoption"); + binding.btnDeleteAdoption.setVisibility(View.GONE); + binding.tvAdoptionId.setVisibility(View.GONE); } } @@ -176,20 +162,20 @@ public class AdoptionDetailFragment extends Fragment { * Validates input and saves the adoption request to the backend. */ private void saveAdoption() { - if (spinnerCustomer.getSelectedItemPosition() == 0) { + if (binding.spinnerAdoptionCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; } - if (spinnerPet.getSelectedItemPosition() == 0) { + if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; } - String date = etAdoptionDate.getText().toString().trim(); + String date = binding.etAdoptionDate.getText().toString().trim(); if (date.isEmpty()) { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; } - CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); - String status = STATUSES[spinnerStatus.getSelectedItemPosition()]; + CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); + PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( pet.getPetId(), diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index f137bdfd..83e70852 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -10,7 +10,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentAppointmentDetailBinding; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.Resource; @@ -31,11 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class AppointmentDetailFragment extends Fragment { - private TextView tvMode, tvAppointmentId; - private EditText etAppointmentDate; - private Spinner spinnerPet, spinnerService, spinnerStatus, spinnerHour, spinnerMinute; - private Spinner spinnerCustomer, spinnerStore; - private Button btnSave, btnDelete, btnBack; + private FragmentAppointmentDetailBinding binding; private long appointmentId = -1; private boolean isEditing = false; @@ -71,59 +67,45 @@ public class AppointmentDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); - initViews(view); + binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); setupSpinners(); setupDatePicker(); loadData(); handleArguments(); - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> saveAppointment()); - btnDelete.setOnClickListener(v -> confirmDelete()); - return view; + binding.btnApptBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment()); + binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); + return binding.getRoot(); } - /** - * Initializes UI components from the layout. - */ - private void initViews(View v) { - tvMode = v.findViewById(R.id.tvApptMode); - tvAppointmentId = v.findViewById(R.id.tvAppointmentId); - etAppointmentDate= v.findViewById(R.id.etAppointmentDate); - spinnerPet = v.findViewById(R.id.spinnerPet); - spinnerService = v.findViewById(R.id.spinnerService); - spinnerStatus = v.findViewById(R.id.spinnerAppointmentStatus); - spinnerHour = v.findViewById(R.id.spinnerHour); - spinnerMinute = v.findViewById(R.id.spinnerMinute); - spinnerCustomer = v.findViewById(R.id.spinnerCustomer); - spinnerStore = v.findViewById(R.id.spinnerStore); - btnSave = v.findViewById(R.id.btnSaveAppointment); - btnDelete = v.findViewById(R.id.btnDeleteAppointment); - btnBack = v.findViewById(R.id.btnApptBack); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * Configures the adapters for spinners. */ private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), spinnerStatus, STATUSES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, STATUSES); String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) hours[i] = String.format("%02d:00", HOURS[i]); - SpinnerUtils.setupStringSpinner(requireContext(), spinnerHour, hours); - SpinnerUtils.setupStringSpinner(requireContext(), spinnerMinute, new String[]{"00","15","30","45"}); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerHour, hours); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerMinute, new String[]{"00","15","30","45"}); } /** * Configures the date picker dialog for the appointment date field. */ private void setupDatePicker() { - etAppointmentDate.setOnClickListener(v -> { + binding.etAppointmentDate.setOnClickListener(v -> { Calendar c = Calendar.getInstance(); DatePickerDialog d = new DatePickerDialog(requireContext(), - (dp,y,m,d1) -> etAppointmentDate.setText( + (dp,y,m,d1) -> binding.etAppointmentDate.setText( String.format("%04d-%02d-%02d", y, m+1, d1)), c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)); @@ -149,7 +131,7 @@ public class AppointmentDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerPet, petList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, PetDTO::getPetName, "-- Select Pet --", preselectedPetId, PetDTO::getPetId); } @@ -163,7 +145,7 @@ public class AppointmentDetailFragment extends Fragment { serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { serviceList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerService, serviceList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, ServiceDTO::getServiceName, "-- Select Service --", preselectedServiceId, ServiceDTO::getServiceId); } @@ -177,7 +159,7 @@ public class AppointmentDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerCustomer, customerList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, item -> item.getFirstName() + " " + item.getLastName(), "-- Select Customer --", preselectedCustomerId, CustomerDTO::getCustomerId); @@ -192,7 +174,7 @@ public class AppointmentDetailFragment extends Fragment { storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { storeList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerStore, storeList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, StoreDTO::getStoreName, "-- Select Store --", preselectedStoreId, StoreDTO::getStoreId); } @@ -212,11 +194,11 @@ public class AppointmentDetailFragment extends Fragment { preselectedCustomerId = a.getLong("customerId", -1); preselectedStoreId = a.getLong("storeId", -1); - tvMode.setText("Edit Appointment"); - tvAppointmentId.setText("ID: " + appointmentId); - tvAppointmentId.setVisibility(View.VISIBLE); - etAppointmentDate.setText(a.getString("appointmentDate")); - btnDelete.setVisibility(View.VISIBLE); + binding.tvApptMode.setText("Edit Appointment"); + binding.tvAppointmentId.setText("ID: " + appointmentId); + binding.tvAppointmentId.setVisibility(View.VISIBLE); + binding.etAppointmentDate.setText(a.getString("appointmentDate")); + binding.btnDeleteAppointment.setVisibility(View.VISIBLE); // Pre-fill time spinners String time = a.getString("appointmentTime", "09:00"); @@ -226,18 +208,18 @@ public class AppointmentDetailFragment extends Fragment { int hour = Integer.parseInt(parts[0]); int min = Integer.parseInt(parts[1]); for (int i = 0; i < HOURS.length; i++) - if (HOURS[i] == hour) { spinnerHour.setSelection(i); break; } + if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; } for (int i = 0; i < MINUTES.length; i++) - if (MINUTES[i] == min) { spinnerMinute.setSelection(i); break; } + if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; } } // Pre-fill status - SpinnerUtils.setSelectionByValue(spinnerStatus, a.getString("appointmentStatus", "Booked")); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getString("appointmentStatus", "Booked")); } else { - tvMode.setText("Add Appointment"); - btnDelete.setVisibility(View.GONE); - tvAppointmentId.setVisibility(View.GONE); + binding.tvApptMode.setText("Add Appointment"); + binding.btnDeleteAppointment.setVisibility(View.GONE); + binding.tvAppointmentId.setVisibility(View.GONE); } } @@ -245,32 +227,32 @@ public class AppointmentDetailFragment extends Fragment { * Validates input and saves the appointment to the backend. */ private void saveAppointment() { - if (spinnerCustomer.getSelectedItemPosition() == 0) { + if (binding.spinnerCustomer.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a customer", Toast.LENGTH_SHORT).show(); return; } - if (spinnerStore.getSelectedItemPosition() == 0) { + if (binding.spinnerStore.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; } - if (spinnerPet.getSelectedItemPosition() == 0) { + if (binding.spinnerPet.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; } - if (spinnerService.getSelectedItemPosition() == 0) { + if (binding.spinnerService.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a service", Toast.LENGTH_SHORT).show(); return; } - String date = etAppointmentDate.getText().toString().trim(); + String date = binding.etAppointmentDate.getText().toString().trim(); if (date.isEmpty()) { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; } - CustomerDTO customer = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1); - StoreDTO store = storeList.get(spinnerStore.getSelectedItemPosition() - 1); - PetDTO pet = petList.get(spinnerPet.getSelectedItemPosition() - 1); - ServiceDTO service = serviceList.get(spinnerService.getSelectedItemPosition() - 1); + CustomerDTO customer = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1); + StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); + PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); + ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); String time = String.format("%02d:%02d", - HOURS[spinnerHour.getSelectedItemPosition()], - MINUTES[spinnerMinute.getSelectedItemPosition()]); - String status = STATUSES[spinnerStatus.getSelectedItemPosition()]; + HOURS[binding.spinnerHour.getSelectedItemPosition()], + MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); + String status = STATUSES[binding.spinnerAppointmentStatus.getSelectedItemPosition()]; // Validate future date+time if status is Booked diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index e2b0984d..bafa71c5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -9,9 +9,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.AutoCompleteTextView; -import android.widget.Button; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -20,9 +17,8 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; -import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.InputValidator; @@ -41,10 +37,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class InventoryDetailFragment extends Fragment { - private TextView tvMode, tvInventoryId, tvProductInfo; - private AutoCompleteTextView etProductSearch; - private android.widget.EditText etQuantity; - private Button btnSave, btnDelete, btnBack; + private FragmentInventoryDetailBinding binding; private InventoryViewModel inventoryViewModel; private ProductViewModel productViewModel; @@ -73,44 +66,35 @@ public class InventoryDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); + binding = FragmentInventoryDetailBinding.inflate(inflater, container, false); - initViews(view); setupProductSearch(); handleArguments(); - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> saveInventory()); - btnDelete.setOnClickListener(v -> confirmDelete()); - - return view; - } - - /** - * get the layout view and set adapter. - */ - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvInventoryMode); - tvInventoryId = view.findViewById(R.id.tvInventoryId); - tvProductInfo = view.findViewById(R.id.tvProductInfo); - etProductSearch = view.findViewById(R.id.etProductSearch); - etQuantity = view.findViewById(R.id.etQuantity); - btnSave = view.findViewById(R.id.btnSaveInventory); - btnDelete = view.findViewById(R.id.btnDeleteInventory); - btnBack = view.findViewById(R.id.btnInventoryBack); + binding.btnInventoryBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveInventory.setOnClickListener(v -> saveInventory()); + binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); // Setup dropdown adapter dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); - etProductSearch.setAdapter(dropdownAdapter); - etProductSearch.setThreshold(1); // start showing after 1 character + binding.etProductSearch.setAdapter(dropdownAdapter); + binding.etProductSearch.setThreshold(1); // start showing after 1 character + + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** * setup the product search dropdown. */ private void setupProductSearch() { - etProductSearch.addTextChangedListener(new TextWatcher() { + binding.etProductSearch.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } @@ -123,7 +107,7 @@ public class InventoryDetailFragment extends Fragment { public void onTextChanged(CharSequence s, int start, int before, int count) { // Clear selected product when user is typing again selectedProduct = null; - tvProductInfo.setVisibility(View.GONE); + binding.tvProductInfo.setVisibility(View.GONE); if (searchRunnable != null) searchHandler.removeCallbacks(searchRunnable); @@ -137,14 +121,14 @@ public class InventoryDetailFragment extends Fragment { }); // When user picks an item from the dropdown - etProductSearch.setOnItemClickListener((parent, view, position, id) -> { + binding.etProductSearch.setOnItemClickListener((parent, view, position, id) -> { if (position < productSuggestions.size()) { selectedProduct = productSuggestions.get(position); // Show product details below the search box - tvProductInfo.setText( + binding.tvProductInfo.setText( "ID: " + selectedProduct.getProdId() + " • " + selectedProduct.getCategoryName()); - tvProductInfo.setVisibility(View.VISIBLE); + binding.tvProductInfo.setVisibility(View.VISIBLE); } }); } @@ -167,7 +151,7 @@ public class InventoryDetailFragment extends Fragment { dropdownAdapter.clear(); dropdownAdapter.addAll(names); dropdownAdapter.notifyDataSetChanged(); - etProductSearch.showDropDown(); + binding.etProductSearch.showDropDown(); } }); } @@ -181,37 +165,37 @@ public class InventoryDetailFragment extends Fragment { isEditing = true; inventoryId = args.getLong("inventoryId"); - tvMode.setText("Edit Inventory"); - tvInventoryId.setText("Inventory ID: " + inventoryId); - tvInventoryId.setVisibility(View.VISIBLE); + binding.tvInventoryMode.setText("Edit Inventory"); + binding.tvInventoryId.setText("Inventory ID: " + inventoryId); + binding.tvInventoryId.setVisibility(View.VISIBLE); // Pre-fill search box with existing product name String productName = args.getString("productName", ""); long prodId = args.getLong("prodId", -1); - etProductSearch.setText(productName); + binding.etProductSearch.setText(productName); // Show existing product info if (prodId != -1) { - tvProductInfo.setText( + binding.tvProductInfo.setText( "ID: " + prodId + " • " + args.getString("categoryName", "")); - tvProductInfo.setVisibility(View.VISIBLE); + binding.tvProductInfo.setVisibility(View.VISIBLE); // Build a minimal ProductDTO so selectedProduct is not null on save selectedProduct = new ProductDTO(productName, null, null, null); selectedProduct.setProdId(prodId); } - etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); - btnDelete.setVisibility(View.VISIBLE); - btnSave.setText("Save"); + binding.etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); + binding.btnDeleteInventory.setVisibility(View.VISIBLE); + binding.btnSaveInventory.setText("Save"); } else { isEditing = false; - tvMode.setText("Add Inventory"); - tvInventoryId.setVisibility(View.GONE); - tvProductInfo.setVisibility(View.GONE); - btnDelete.setVisibility(View.GONE); - btnSave.setText("Add"); + binding.tvInventoryMode.setText("Add Inventory"); + binding.tvInventoryId.setVisibility(View.GONE); + binding.tvProductInfo.setVisibility(View.GONE); + binding.btnDeleteInventory.setVisibility(View.GONE); + binding.btnSaveInventory.setText("Add"); } } @@ -220,17 +204,17 @@ public class InventoryDetailFragment extends Fragment { */ private void saveInventory() { if (selectedProduct == null) { - etProductSearch.setError("Please select a product from the list"); - etProductSearch.requestFocus(); + binding.etProductSearch.setError("Please select a product from the list"); + binding.etProductSearch.requestFocus(); return; } - if (!InputValidator.isNotEmpty(etQuantity, "Quantity") || - !InputValidator.isPositiveInteger(etQuantity, "Quantity")) { + if (!InputValidator.isNotEmpty(binding.etQuantity, "Quantity") || + !InputValidator.isPositiveInteger(binding.etQuantity, "Quantity")) { return; } - int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); + int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); setButtonsEnabled(false); @@ -297,8 +281,8 @@ public class InventoryDetailFragment extends Fragment { * Enables or disables action buttons. */ private void setButtonsEnabled(boolean enabled) { - btnSave.setEnabled(enabled); - btnDelete.setEnabled(enabled); - btnBack.setEnabled(enabled); + binding.btnSaveInventory.setEnabled(enabled); + binding.btnDeleteInventory.setEnabled(enabled); + binding.btnInventoryBack.setEnabled(enabled); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 23071db6..9af2b79d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -10,13 +10,10 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; @@ -33,10 +30,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class PetDetailFragment extends Fragment { - private TextView tvMode, tvPetId; - private EditText etPetName, etPetSpecies, etPetBreed, etPetAge, etPetPrice; - private Spinner spinnerPetStatus; - private Button btnSavePet, btnDeletePet, btnBack; + private FragmentPetDetailBinding binding; private int petId; private boolean isEditing = false; @@ -49,21 +43,25 @@ public class PetDetailFragment extends Fragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_pet_detail, container, false); + binding = FragmentPetDetailBinding.inflate(inflater, container, false); - //set up spinner and get controls from layout and display the view depending on the mode - initViews(view); setupSpinner(); handleArguments(); //set button click listeners - btnBack.setOnClickListener(v -> navigateBack()); - btnSavePet.setOnClickListener(v -> savePet()); - btnDeletePet.setOnClickListener(v -> deletePet()); + binding.btnBack.setOnClickListener(v -> navigateBack()); + binding.btnSavePet.setOnClickListener(v -> savePet()); + binding.btnDeletePet.setOnClickListener(v -> deletePet()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -71,19 +69,19 @@ public class PetDetailFragment extends Fragment { */ private void savePet() { // Validates all fields using InputValidator - if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; - if (!InputValidator.isNotEmpty(etPetSpecies, "Species")) return; - if (!InputValidator.isNotEmpty(etPetBreed, "Breed")) return; - if (!InputValidator.isPositiveInteger(etPetAge, "Age")) return; - if (!InputValidator.isPositiveDecimal(etPetPrice, "Price")) return; + if (!InputValidator.isNotEmpty(binding.etPetName, "Pet Name")) return; + if (!InputValidator.isNotEmpty(binding.etPetSpecies, "Species")) return; + if (!InputValidator.isNotEmpty(binding.etPetBreed, "Breed")) return; + if (!InputValidator.isPositiveInteger(binding.etPetAge, "Age")) return; + if (!InputValidator.isPositiveDecimal(binding.etPetPrice, "Price")) return; //get all the values from the fields - String name = etPetName.getText().toString().trim(); - String species = etPetSpecies.getText().toString().trim(); - String breed = etPetBreed.getText().toString().trim(); - int age = Integer.parseInt(etPetAge.getText().toString().trim()); - String priceStr = etPetPrice.getText().toString().trim(); - String status = spinnerPetStatus.getSelectedItem().toString(); + String name = binding.etPetName.getText().toString().trim(); + String species = binding.etPetSpecies.getText().toString().trim(); + String breed = binding.etPetBreed.getText().toString().trim(); + int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); + String priceStr = binding.etPetPrice.getText().toString().trim(); + String status = binding.spinnerPetStatus.getSelectedItem().toString(); //create a pet object to send to the API PetDTO petDTO = new PetDTO(); @@ -160,48 +158,31 @@ public class PetDetailFragment extends Fragment { // Get pet data from arguments and populate fields isEditing = true; petId = getArguments().getInt("petId"); - tvMode.setText("Edit Pet"); - tvPetId.setText("ID: " + petId); - etPetName.setText(getArguments().getString("petName")); - etPetSpecies.setText(getArguments().getString("petSpecies")); - etPetBreed.setText(getArguments().getString("petBreed")); - etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); - etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); - SpinnerUtils.setSelectionByValue(spinnerPetStatus, getArguments().getString("petStatus")); - btnDeletePet.setVisibility(View.VISIBLE); + binding.tvMode.setText("Edit Pet"); + binding.tvPetId.setText("ID: " + petId); + binding.etPetName.setText(getArguments().getString("petName")); + binding.etPetSpecies.setText(getArguments().getString("petSpecies")); + binding.etPetBreed.setText(getArguments().getString("petBreed")); + binding.etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); + binding.etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); + SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, getArguments().getString("petStatus")); + binding.btnDeletePet.setVisibility(View.VISIBLE); } else { // Pet is being added // Set default values for add a new pet isEditing = false; - tvMode.setText("Add Pet"); - tvPetId.setVisibility(View.GONE); - btnDeletePet.setVisibility(View.GONE); - btnSavePet.setText("Add"); + binding.tvMode.setText("Add Pet"); + binding.tvPetId.setVisibility(View.GONE); + binding.btnDeletePet.setVisibility(View.GONE); + binding.btnSavePet.setText("Add"); } } - /** - * Binds UI components from the layout. - */ - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvMode); - tvPetId = view.findViewById(R.id.tvPetId); - etPetName = view.findViewById(R.id.etPetName); - etPetSpecies = view.findViewById(R.id.etPetSpecies); - etPetBreed = view.findViewById(R.id.etPetBreed); - etPetAge = view.findViewById(R.id.etPetAge); - etPetPrice = view.findViewById(R.id.etPetPrice); - spinnerPetStatus = view.findViewById(R.id.spinnerPetStatus); - btnSavePet = view.findViewById(R.id.btnSavePet); - btnDeletePet = view.findViewById(R.id.btnDeletePet); - btnBack = view.findViewById(R.id.btnBack); - } - /** * Initializes the spinner for pet status selection. */ private void setupSpinner() { - SpinnerUtils.setupStringSpinner(requireContext(), spinnerPetStatus, + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, new String[]{"Available", "Adopted"}); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 64f2c7f4..4784fd03 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -13,10 +13,10 @@ import com.bumptech.glide.Glide; import com.example.petstoremobile.R; import com.example.petstoremobile.api.*; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentProductDetailBinding; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.viewmodels.ProductViewModel; import com.example.petstoremobile.utils.DialogUtils; -import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; @@ -42,11 +42,7 @@ import okhttp3.RequestBody; @AndroidEntryPoint public class ProductDetailFragment extends Fragment { - private TextView tvMode, tvProductId; - private EditText etProductName, etProductDesc, etProductPrice; - private Spinner spinnerCategory; - private Button btnSave, btnDelete, btnBack; - private ImageView ivProductImage; + private FragmentProductDetailBinding binding; private long prodId = -1; private boolean isEditing = false; @@ -75,7 +71,7 @@ public class ProductDetailFragment extends Fragment { @Override public void onImagePicked(Uri uri) { photoUri = uri; - Glide.with(ProductDetailFragment.this).load(uri).into(ivProductImage); + Glide.with(ProductDetailFragment.this).load(uri).into(binding.ivProductImage); hasImage = true; isImageChanged = true; isImageRemoved = false; @@ -87,7 +83,7 @@ public class ProductDetailFragment extends Fragment { hasImage = false; isImageChanged = false; isImageRemoved = true; - ivProductImage.setImageResource(R.drawable.placeholder2); + binding.ivProductImage.setImageResource(R.drawable.placeholder2); } }); } @@ -98,32 +94,22 @@ public class ProductDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_product_detail, container, false); - initViews(view); + binding = FragmentProductDetailBinding.inflate(inflater, container, false); + loadCategories(); handleArguments(); - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> saveProduct()); - btnDelete.setOnClickListener(v -> confirmDelete()); - ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); - return view; + binding.btnProductBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveProduct.setOnClickListener(v -> saveProduct()); + binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete()); + binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); + return binding.getRoot(); } - /** - * get the UI components from the layout. - */ - private void initViews(View v) { - tvMode = v.findViewById(R.id.tvProductMode); - tvProductId = v.findViewById(R.id.tvProductId); - etProductName = v.findViewById(R.id.etProductName); - etProductDesc = v.findViewById(R.id.etProductDesc); - etProductPrice = v.findViewById(R.id.etProductPrice); - spinnerCategory = v.findViewById(R.id.spinnerProductCategory); - btnSave = v.findViewById(R.id.btnSaveProduct); - btnDelete = v.findViewById(R.id.btnDeleteProduct); - btnBack = v.findViewById(R.id.btnProductBack); - ivProductImage = v.findViewById(R.id.ivProductImage); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -133,7 +119,7 @@ public class ProductDetailFragment extends Fragment { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { categoryList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerCategory, categoryList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, CategoryDTO::getCategoryName, "-- Select Category --", preselectedCategoryId, CategoryDTO::getCategoryId); } @@ -151,18 +137,18 @@ public class ProductDetailFragment extends Fragment { preselectedCategoryId = a.getLong("categoryId", -1); hasImage = true; - tvMode.setText("Edit Product"); - tvProductId.setText("ID: " + prodId); - tvProductId.setVisibility(View.VISIBLE); - etProductName.setText(a.getString("prodName")); - etProductDesc.setText(a.getString("prodDesc")); - etProductPrice.setText(a.getString("prodPrice")); - btnDelete.setVisibility(View.VISIBLE); + binding.tvProductMode.setText("Edit Product"); + binding.tvProductId.setText("ID: " + prodId); + binding.tvProductId.setVisibility(View.VISIBLE); + binding.etProductName.setText(a.getString("prodName")); + binding.etProductDesc.setText(a.getString("prodDesc")); + binding.etProductPrice.setText(a.getString("prodPrice")); + binding.btnDeleteProduct.setVisibility(View.VISIBLE); loadProductImage(); } else { - tvMode.setText("Add Product"); - btnDelete.setVisibility(View.GONE); - tvProductId.setVisibility(View.GONE); + binding.tvProductMode.setText("Add Product"); + binding.btnDeleteProduct.setVisibility(View.GONE); + binding.tvProductId.setVisibility(View.GONE); hasImage = false; } } @@ -174,7 +160,7 @@ public class ProductDetailFragment extends Fragment { String imageUrl = baseUrl + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { + GlideUtils.loadImageWithToken(requireContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { @Override public void onResourceReady() { hasImage = true; @@ -240,22 +226,22 @@ public class ProductDetailFragment extends Fragment { * Validates input fields and saves product information to the backend. */ private void saveProduct() { - if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return; + if (!InputValidator.isNotEmpty(binding.etProductName, "Product Name")) return; - if (spinnerCategory.getSelectedItemPosition() == 0) { + if (binding.spinnerProductCategory.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a category", Toast.LENGTH_SHORT).show(); return; } - if (!InputValidator.isNotEmpty(etProductPrice, "Price") || - !InputValidator.isPositiveDecimal(etProductPrice, "Price")) { + if (!InputValidator.isNotEmpty(binding.etProductPrice, "Price") || + !InputValidator.isPositiveDecimal(binding.etProductPrice, "Price")) { return; } - String name = etProductName.getText().toString().trim(); - String desc = etProductDesc.getText().toString().trim(); - BigDecimal price = new BigDecimal(etProductPrice.getText().toString().trim()); + String name = binding.etProductName.getText().toString().trim(); + String desc = binding.etProductDesc.getText().toString().trim(); + BigDecimal price = new BigDecimal(binding.etProductPrice.getText().toString().trim()); - CategoryDTO category = categoryList.get(spinnerCategory.getSelectedItemPosition() - 1); + CategoryDTO category = categoryList.get(binding.spinnerProductCategory.getSelectedItemPosition() - 1); ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); if (isEditing) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index bee0873d..4fd23fa0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentProductSupplierDetailBinding; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; @@ -29,10 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ProductSupplierDetailFragment extends Fragment { - private TextView tvMode; - private Spinner spinnerProduct, spinnerSupplier; - private EditText etCost; - private Button btnSave, btnDelete, btnBack; + private FragmentProductSupplierDetailBinding binding; private boolean isEditing = false; private long editProductId = -1; @@ -58,28 +55,20 @@ public class ProductSupplierDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_product_supplier_detail, container, false); - initViews(view); + binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false); loadData(); handleArguments(); - btnBack.setOnClickListener(v -> navigateBack()); - btnSave.setOnClickListener(v -> save()); - btnDelete.setOnClickListener(v -> confirmDelete()); - return view; + binding.btnPSBack.setOnClickListener(v -> navigateBack()); + binding.btnSavePS.setOnClickListener(v -> save()); + binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); + return binding.getRoot(); } - /** - * Initializes UI components from the layout. - */ - private void initViews(View v) { - tvMode = v.findViewById(R.id.tvPSMode); - spinnerProduct = v.findViewById(R.id.spinnerPSProduct); - spinnerSupplier = v.findViewById(R.id.spinnerPSSupplier); - etCost = v.findViewById(R.id.etPSCost); - btnSave = v.findViewById(R.id.btnSavePS); - btnDelete = v.findViewById(R.id.btnDeletePS); - btnBack = v.findViewById(R.id.btnPSBack); + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -97,7 +86,7 @@ public class ProductSupplierDetailFragment extends Fragment { productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerProduct, productList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, ProductDTO::getProdName, "-- Select Product --", preselectedProductId, ProductDTO::getProdId); } @@ -111,7 +100,7 @@ public class ProductSupplierDetailFragment extends Fragment { supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { supplierList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), spinnerSupplier, supplierList, + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, SupplierDTO::getSupCompany, "-- Select Supplier --", preselectedSupplierId, SupplierDTO::getSupId); } @@ -129,12 +118,12 @@ public class ProductSupplierDetailFragment extends Fragment { editSupplierId = a.getLong("supplierId"); preselectedProductId = editProductId; preselectedSupplierId = editSupplierId; - etCost.setText(a.getString("cost")); - tvMode.setText("Edit Product Supplier"); - btnDelete.setVisibility(View.VISIBLE); + binding.etPSCost.setText(a.getString("cost")); + binding.tvPSMode.setText("Edit Product Supplier"); + binding.btnDeletePS.setVisibility(View.VISIBLE); } else { - tvMode.setText("Add Product Supplier"); - btnDelete.setVisibility(View.GONE); + binding.tvPSMode.setText("Add Product Supplier"); + binding.btnDeletePS.setVisibility(View.GONE); } } @@ -142,21 +131,21 @@ public class ProductSupplierDetailFragment extends Fragment { * Validates input and saves the product-supplier to the backend. */ private void save() { - if (spinnerProduct.getSelectedItemPosition() == 0) { + if (binding.spinnerPSProduct.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); return; } - if (spinnerSupplier.getSelectedItemPosition() == 0) { + if (binding.spinnerPSSupplier.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a supplier", Toast.LENGTH_SHORT).show(); return; } - if (!InputValidator.isNotEmpty(etCost, "Cost") || - !InputValidator.isPositiveDecimal(etCost, "Cost")) { + if (!InputValidator.isNotEmpty(binding.etPSCost, "Cost") || + !InputValidator.isPositiveDecimal(binding.etPSCost, "Cost")) { return; } - ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); - SupplierDTO supplier = supplierList.get(spinnerSupplier.getSelectedItemPosition() - 1); - BigDecimal cost = new BigDecimal(etCost.getText().toString().trim()); + ProductDTO product = productList.get(binding.spinnerPSProduct.getSelectedItemPosition() - 1); + SupplierDTO supplier = supplierList.get(binding.spinnerPSSupplier.getSelectedItemPosition() - 1); + BigDecimal cost = new BigDecimal(binding.etPSCost.getText().toString().trim()); ProductSupplierDTO dto = new ProductSupplierDTO( product.getProdId(), supplier.getSupId(), cost); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index e4f05a26..5d9cf5a9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -3,12 +3,11 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.graphics.Color; import android.os.Bundle; import android.view.*; -import android.widget.*; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; import dagger.hilt.android.AndroidEntryPoint; @@ -18,8 +17,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class PurchaseOrderDetailFragment extends Fragment { - private TextView tvId, tvSupplier, tvDate, tvStatus; - private Button btnBack; + private FragmentPurchaseOrderDetailBinding binding; /** * Inflates the layout, initializes views, and populates order data from arguments. @@ -27,38 +25,38 @@ public class PurchaseOrderDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_purchase_order_detail, container, false); - - tvId = view.findViewById(R.id.tvPODetailId); - tvSupplier = view.findViewById(R.id.tvPODetailSupplier); - tvDate = view.findViewById(R.id.tvPODetailDate); - tvStatus = view.findViewById(R.id.tvPODetailStatus); - btnBack = view.findViewById(R.id.btnPOBack); + binding = FragmentPurchaseOrderDetailBinding.inflate(inflater, container, false); Bundle a = getArguments(); if (a != null) { - tvId.setText("PO #" + a.getLong("purchaseOrderId")); - tvSupplier.setText(a.getString("supplierName")); - tvDate.setText(a.getString("orderDate")); + binding.tvPODetailId.setText("PO #" + a.getLong("purchaseOrderId")); + binding.tvPODetailSupplier.setText(a.getString("supplierName")); + binding.tvPODetailDate.setText(a.getString("orderDate")); String status = a.getString("status", ""); - tvStatus.setText(status); + binding.tvPODetailStatus.setText(status); switch (status) { case "Completed": - tvStatus.setTextColor(Color.parseColor("#4CAF50")); break; + binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; case "Pending": - tvStatus.setTextColor(Color.parseColor("#FF9800")); break; + binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; case "Cancelled": - tvStatus.setTextColor(Color.parseColor("#F44336")); break; + binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; default: - tvStatus.setTextColor(Color.parseColor("#9E9E9E")); break; + binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; } } - btnBack.setOnClickListener(v -> { + binding.btnPOBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index c607c6de..f4ce5f91 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -1,21 +1,17 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; import android.widget.Toast; -import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; + import com.example.petstoremobile.api.SaleApi; +import com.example.petstoremobile.databinding.FragmentRefundDetailBinding; import com.example.petstoremobile.fragments.listfragments.SaleFragment; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.InputValidator; @@ -28,10 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class RefundDetailFragment extends Fragment { - private EditText etRefundSaleId, etRefundReason; - private TextView tvSaleInfo; - private Spinner spinnerRefundPayment; - private Button btnLoadSale, btnProcessRefund, btnBack; + private FragmentRefundDetailBinding binding; private int saleId; private SaleFragment saleFragment; @@ -42,23 +35,28 @@ public class RefundDetailFragment extends Fragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_refund_detail, container, false); + binding = FragmentRefundDetailBinding.inflate(inflater, container, false); - initViews(view); setupSpinner(); handleArguments(); - btnBack.setOnClickListener(v -> goBack()); - btnLoadSale.setOnClickListener(v -> loadSaleDetails()); - btnProcessRefund.setOnClickListener(v -> processRefund()); + binding.btnRefundBack.setOnClickListener(v -> goBack()); + binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails()); + binding.btnProcessRefund.setOnClickListener(v -> processRefund()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } private void loadSaleDetails() { - String idText = etRefundSaleId.getText().toString().trim(); + String idText = binding.etRefundSaleId.getText().toString().trim(); if (idText.isEmpty()) { Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); return; @@ -68,22 +66,21 @@ public class RefundDetailFragment extends Fragment { int id = Integer.parseInt(idText); // TODO: Replace with actual API call - GET v1/sales/{id} // For now show placeholder info - tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund."); - tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); + binding.tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund."); + binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); } catch (NumberFormatException e) { Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); } } private void processRefund() { - if (!InputValidator.isNotEmpty(etRefundSaleId, "Sale ID")) + if (!InputValidator.isNotEmpty(binding.etRefundSaleId, "Sale ID")) return; - if (!InputValidator.isNotEmpty(etRefundReason, "Refund Reason")) + if (!InputValidator.isNotEmpty(binding.etRefundReason, "Refund Reason")) return; - String idText = etRefundSaleId.getText().toString().trim(); - String reason = etRefundReason.getText().toString().trim(); - String payment = spinnerRefundPayment.getSelectedItem().toString(); + String idText = binding.etRefundSaleId.getText().toString().trim(); + String reason = binding.etRefundReason.getText().toString().trim(); try { int id = Integer.parseInt(idText); @@ -101,16 +98,16 @@ public class RefundDetailFragment extends Fragment { private void handleArguments() { if (getArguments() != null && getArguments().containsKey("saleId")) { saleId = getArguments().getInt("saleId"); - etRefundSaleId.setText(String.valueOf(saleId)); + binding.etRefundSaleId.setText(String.valueOf(saleId)); String info = "Sale Date: " + getArguments().getString("saleDate") + " | Employee: " + getArguments().getString("employeeName") + " | Total: $" + String.format("%.2f", getArguments().getDouble("total")) + " | Payment: " + getArguments().getString("paymentMethod"); - tvSaleInfo.setText(info); - tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); + binding.tvSaleInfo.setText(info); + binding.tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); // Pre-select payment method - SpinnerUtils.setSelectionByValue(spinnerRefundPayment, getArguments().getString("paymentMethod")); + SpinnerUtils.setSelectionByValue(binding.spinnerRefundPayment, getArguments().getString("paymentMethod")); } } @@ -118,18 +115,8 @@ public class RefundDetailFragment extends Fragment { NavHostFragment.findNavController(this).popBackStack(); } - private void initViews(View view) { - etRefundSaleId = view.findViewById(R.id.etRefundSaleId); - etRefundReason = view.findViewById(R.id.etRefundReason); - tvSaleInfo = view.findViewById(R.id.tvSaleInfo); - spinnerRefundPayment = view.findViewById(R.id.spinnerRefundPayment); - btnLoadSale = view.findViewById(R.id.btnLoadSale); - btnProcessRefund = view.findViewById(R.id.btnProcessRefund); - btnBack = view.findViewById(R.id.btnRefundBack); - } - private void setupSpinner() { - SpinnerUtils.setupStringSpinner(requireContext(), spinnerRefundPayment, + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerRefundPayment, new String[] { "Cash", "Card", "Debit" }); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 5cb9a7c2..7f9c053b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -10,12 +10,10 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentServiceDetailBinding; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; @@ -31,9 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class ServiceDetailFragment extends Fragment { - private TextView tvMode, tvServiceId; - private EditText etServiceName, etServiceDesc, etServiceDuration, etServicePrice; - private Button btnSaveService, btnDeleteService, btnBack; + private FragmentServiceDetailBinding binding; private int serviceId; private boolean isEditing = false; @@ -48,18 +44,23 @@ public class ServiceDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_service_detail, container, false); + binding = FragmentServiceDetailBinding.inflate(inflater, container, false); //get controls from layout and display the view depending on the mode - initViews(view); handleArguments(); //set button click listeners - btnBack.setOnClickListener(v -> navigateBack()); - btnSaveService.setOnClickListener(v -> saveService()); - btnDeleteService.setOnClickListener(v -> deleteService()); + binding.btnBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveService.setOnClickListener(v -> saveService()); + binding.btnDeleteService.setOnClickListener(v -> deleteService()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -67,16 +68,16 @@ public class ServiceDetailFragment extends Fragment { */ private void saveService() { // Validates all fields using InputValidator - if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return; - if (!InputValidator.isNotEmpty(etServiceDesc, "Description")) return; - if (!InputValidator.isPositiveInteger(etServiceDuration, "Duration")) return; - if (!InputValidator.isPositiveDecimal(etServicePrice, "Price")) return; + if (!InputValidator.isNotEmpty(binding.etServiceName, "Service Name")) return; + if (!InputValidator.isNotEmpty(binding.etServiceDesc, "Description")) return; + if (!InputValidator.isPositiveInteger(binding.etServiceDuration, "Duration")) return; + if (!InputValidator.isPositiveDecimal(binding.etServicePrice, "Price")) return; //get all the values from the fields - String name = etServiceName.getText().toString().trim(); - String desc = etServiceDesc.getText().toString().trim(); - int duration = Integer.parseInt(etServiceDuration.getText().toString().trim()); - double price = Double.parseDouble(etServicePrice.getText().toString().trim()); + String name = binding.etServiceName.getText().toString().trim(); + String desc = binding.etServiceDesc.getText().toString().trim(); + int duration = Integer.parseInt(binding.etServiceDuration.getText().toString().trim()); + double price = Double.parseDouble(binding.etServicePrice.getText().toString().trim()); //create a service object to send to the API ServiceDTO serviceDTO = new ServiceDTO(); @@ -143,36 +144,21 @@ public class ServiceDetailFragment extends Fragment { // Get service data from arguments and populate fields isEditing = true; serviceId = getArguments().getInt("serviceId"); - tvMode.setText("Edit Service"); - tvServiceId.setText("ID: " + serviceId); - etServiceName.setText(getArguments().getString("serviceName")); - etServiceDesc.setText(getArguments().getString("serviceDesc")); - etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration"))); - etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice"))); - btnDeleteService.setVisibility(View.VISIBLE); + binding.tvMode.setText("Edit Service"); + binding.tvServiceId.setText("ID: " + serviceId); + binding.etServiceName.setText(getArguments().getString("serviceName")); + binding.etServiceDesc.setText(getArguments().getString("serviceDesc")); + binding.etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration"))); + binding.etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice"))); + binding.btnDeleteService.setVisibility(View.VISIBLE); } else { // Service is being added // Set default values for add a new service isEditing = false; - tvMode.setText("Add Service"); - tvServiceId.setVisibility(View.GONE); - btnDeleteService.setVisibility(View.GONE); - btnSaveService.setText("Add"); + binding.tvMode.setText("Add Service"); + binding.tvServiceId.setVisibility(View.GONE); + binding.btnDeleteService.setVisibility(View.GONE); + binding.btnSaveService.setText("Add"); } } - - /** - * Initializes UI components from the layout. - */ - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvMode); - tvServiceId = view.findViewById(R.id.tvServiceId); - etServiceName = view.findViewById(R.id.etServiceName); - etServiceDesc = view.findViewById(R.id.etServiceDesc); - etServiceDuration = view.findViewById(R.id.etServiceDuration); - etServicePrice = view.findViewById(R.id.etServicePrice); - btnSaveService = view.findViewById(R.id.btnSaveService); - btnDeleteService = view.findViewById(R.id.btnDeleteService); - btnBack = view.findViewById(R.id.btnBack); - } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 4477712f..5d52606d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -10,12 +10,9 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.FragmentSupplierDetailBinding; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; @@ -32,9 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class SupplierDetailFragment extends Fragment { - private TextView tvMode, tvSupId; - private EditText etSupCompany, etSupContactFirstName, etSupContactLastName, etSupEmail, etSupPhone; - private Button btnSaveSupplier, btnDeleteSupplier, btnBack; + private FragmentSupplierDetailBinding binding; private int supId; private boolean isEditing = false; @@ -49,18 +44,25 @@ public class SupplierDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_supplier_detail, container, false); + binding = FragmentSupplierDetailBinding.inflate(inflater, container, false); + + // Add phone number formatting (CA) and limit length to 14 characters + UIUtils.formatPhoneInput(binding.etSupPhone); - //get controls from layout and display the view depending on the mode - initViews(view); handleArguments(); //set button click listeners - btnBack.setOnClickListener(v -> navigateBack()); - btnSaveSupplier.setOnClickListener(v -> saveSupplier()); - btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); + binding.btnBack.setOnClickListener(v -> navigateBack()); + binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); + binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** @@ -68,18 +70,18 @@ public class SupplierDetailFragment extends Fragment { */ private void saveSupplier() { // Validates all fields using InputValidator - if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return; - if (!InputValidator.isNotEmpty(etSupContactFirstName, "First Name")) return; - if (!InputValidator.isNotEmpty(etSupContactLastName, "Last Name")) return; - if (!InputValidator.isValidEmail(etSupEmail)) return; - if (!InputValidator.isValidPhone(etSupPhone)) return; + if (!InputValidator.isNotEmpty(binding.etSupCompany, "Company Name")) return; + if (!InputValidator.isNotEmpty(binding.etSupContactFirstName, "First Name")) return; + if (!InputValidator.isNotEmpty(binding.etSupContactLastName, "Last Name")) return; + if (!InputValidator.isValidEmail(binding.etSupEmail)) return; + if (!InputValidator.isValidPhone(binding.etSupPhone)) return; //get all the values from the fields - String company = etSupCompany.getText().toString().trim(); - String firstName = etSupContactFirstName.getText().toString().trim(); - String lastName = etSupContactLastName.getText().toString().trim(); - String email = etSupEmail.getText().toString().trim(); - String phone = etSupPhone.getText().toString().trim(); + String company = binding.etSupCompany.getText().toString().trim(); + String firstName = binding.etSupContactFirstName.getText().toString().trim(); + String lastName = binding.etSupContactLastName.getText().toString().trim(); + String email = binding.etSupEmail.getText().toString().trim(); + String phone = binding.etSupPhone.getText().toString().trim(); //create a supplier object to send to the API SupplierDTO supplierDTO = new SupplierDTO(); @@ -148,42 +150,22 @@ public class SupplierDetailFragment extends Fragment { // Get supplier data from arguments and populate fields isEditing = true; supId = getArguments().getInt("supId"); - tvMode.setText("Edit Supplier"); - tvSupId.setText("ID: " + supId); - etSupCompany.setText(getArguments().getString("supCompany")); - etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); - etSupContactLastName.setText(getArguments().getString("supContactLastName")); - etSupEmail.setText(getArguments().getString("supEmail")); - etSupPhone.setText(getArguments().getString("supPhone")); - btnDeleteSupplier.setVisibility(View.VISIBLE); + binding.tvMode.setText("Edit Supplier"); + binding.tvSupId.setText("ID: " + supId); + binding.etSupCompany.setText(getArguments().getString("supCompany")); + binding.etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); + binding.etSupContactLastName.setText(getArguments().getString("supContactLastName")); + binding.etSupEmail.setText(getArguments().getString("supEmail")); + binding.etSupPhone.setText(getArguments().getString("supPhone")); + binding.btnDeleteSupplier.setVisibility(View.VISIBLE); } else { // Supplier is being added // Set default values for add a new supplier isEditing = false; - tvMode.setText("Add Supplier"); - tvSupId.setVisibility(View.GONE); - btnDeleteSupplier.setVisibility(View.GONE); - btnSaveSupplier.setText("Add"); + binding.tvMode.setText("Add Supplier"); + binding.tvSupId.setVisibility(View.GONE); + binding.btnDeleteSupplier.setVisibility(View.GONE); + binding.btnSaveSupplier.setText("Add"); } } - - /** - * Initializes the UI components and sets up formatting for phone input. - */ - private void initViews(View view) { - tvMode = view.findViewById(R.id.tvMode); - tvSupId = view.findViewById(R.id.tvSupId); - etSupCompany = view.findViewById(R.id.etSupCompany); - etSupContactFirstName = view.findViewById(R.id.etSupContactFirstName); - etSupContactLastName = view.findViewById(R.id.etSupContactLastName); - etSupEmail = view.findViewById(R.id.etSupEmail); - etSupPhone = view.findViewById(R.id.etSupPhone); - - // Add phone number formatting (CA) and limit length to 14 characters - UIUtils.formatPhoneInput(etSupPhone); - - btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier); - btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); - btnBack = view.findViewById(R.id.btnBack); - } } From a3d454e1191852622385123bc73b870c8f815bb4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 23:35:05 -0600 Subject: [PATCH 067/137] Enforce pet ownership rules --- .../controller/DropdownController.java | 9 +--- .../backend/service/AppointmentService.java | 8 ++-- .../controller/DropdownControllerTest.java | 12 ++--- .../service/AppointmentServiceTest.java | 48 +------------------ 4 files changed, 10 insertions(+), 67 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 2217b4e7..409891bc 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -80,15 +80,8 @@ public class DropdownController { @GetMapping("/appointment-customers") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { - User user = com.petshop.backend.util.AuthenticationHelper.getAuthenticatedUser(userRepository); - List customers; - if (user.getRole() == User.Role.ADMIN) { - customers = customerRepository.findAll(); - } else { - customers = customerRepository.findAllWithPets(); - } return ResponseEntity.ok( - customers.stream() + customerRepository.findAllWithPets().stream() .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) .collect(Collectors.toList()) ); diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 363cb3a0..8c0e05d8 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -122,7 +122,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); Appointment appointment = new Appointment(); @@ -170,7 +170,7 @@ public class AppointmentService { } Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId(), authenticatedUser.getRole()) : new HashSet<>(); + Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); appointment.setCustomer(customer); @@ -255,12 +255,12 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId, User.Role authenticatedRole) { + private Set fetchCustomerPets(List customerPetIds, Long customerId) { Set customerPets = new HashSet<>(); for (Long customerPetId : customerPetIds) { CustomerPet customerPet = customerPetRepository.findById(customerPetId) .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (authenticatedRole != User.Role.ADMIN && !customerPet.getCustomer().getCustomerId().equals(customerId)) { + if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); } customerPets.add(customerPet); diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index 563e5d2f..fa0a0ebe 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -173,7 +173,7 @@ class DropdownControllerTest { } @Test - void getAppointmentCustomersReturnsAllCustomersForAdmin() { + void getAppointmentCustomersReturnsOnlyCustomersWithPetsForAdmin() { User adminUser = new User(); adminUser.setId(88L); adminUser.setRole(User.Role.ADMIN); @@ -185,17 +185,11 @@ class DropdownControllerTest { one.setFirstName("Alex"); one.setLastName("Brown"); - Customer two = new Customer(); - two.setCustomerId(2L); - two.setFirstName("Emily"); - two.setLastName("Clark"); - - when(customerRepository.findAll()).thenReturn(List.of(one, two)); + when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); var response = controller.getAppointmentCustomers(); - assertEquals(2, response.getBody().size()); + assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - assertEquals(Long.valueOf(2L), response.getBody().get(1).getId()); } } diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index d4892126..aecb5644 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -188,7 +188,7 @@ class AppointmentServiceTest { } @Test - void createAppointmentAllowsCustomerPetOwnedByDifferentCustomerForAdmin() { + void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { setAuthentication(99L, User.Role.ADMIN); Customer otherCustomer = new Customer(); @@ -206,11 +206,6 @@ class AppointmentServiceTest { .thenReturn(List.of(new EmployeeStore(employee, store))); when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appt = invocation.getArgument(0); - appt.setAppointmentId(101L); - return appt; - }); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); request.setCustomerId(1L); @@ -221,46 +216,7 @@ class AppointmentServiceTest { request.setAppointmentStatus("Booked"); request.setCustomerPetIds(List.of(22L)); - var response = appointmentService.createAppointment(request); - assertEquals(101L, response.getAppointmentId()); - } - - @Test - void createAppointmentAllowsAnyPetForAdmin() { - setAuthentication(99L, User.Role.ADMIN); - - Customer otherCustomer = new Customer(); - otherCustomer.setCustomerId(22L); - CustomerPet otherCustomerPet = new CustomerPet(); - otherCustomerPet.setCustomerPetId(22L); - otherCustomerPet.setCustomer(otherCustomer); - - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> { - Appointment appointment = invocation.getArgument(0); - appointment.setAppointmentId(101L); - return appointment; - }); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(22L)); - - var response = appointmentService.createAppointment(request); - - assertEquals(101L, response.getAppointmentId()); - assertEquals(1L, response.getCustomerId()); + assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); } @Test From b70afd66aa424170a81acbebeadd01dd3c1b642c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 5 Apr 2026 23:58:21 -0600 Subject: [PATCH 068/137] Allow cross-store staff selection --- .../controller/DropdownController.java | 13 ++++++--- .../repository/EmployeeStoreRepository.java | 3 +++ .../controller/DropdownControllerTest.java | 27 +++++++++++++++++++ .../api/endpoints/DropdownApi.java | 8 ++++++ .../AdoptionDialogController.java | 7 ++++- .../AppointmentDialogController.java | 9 ++++--- 6 files changed, 60 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 409891bc..d0a69036 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -156,13 +156,20 @@ public class DropdownController { ); } - @GetMapping("/stores/{storeId}/employees") + @GetMapping({"/stores/{storeId}/employees", "/employees"}) @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") - public ResponseEntity> getStoreEmployees(@PathVariable Long storeId) { + public ResponseEntity> getStoreEmployees(@PathVariable(required = false) Long storeId) { + List employees; + if (storeId == null || storeId == 0) { + employees = employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc(); + } else { + employees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId); + } return ResponseEntity.ok( - employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + employees.stream() .filter(this::isAssignableEmployee) .map(this::toEmployeeOption) + .distinct() .collect(Collectors.toList()) ); } diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java index 0cc3f771..16a59cea 100644 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java @@ -15,4 +15,7 @@ public interface EmployeeStoreRepository extends JpaRepository findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId); + + @Query("SELECT es FROM EmployeeStore es WHERE es.employee.isActive = true ORDER BY es.employee.employeeId ASC") + List findActiveAllOrderByEmployeeEmployeeIdAsc(); } diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index fa0a0ebe..e62430e6 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -125,6 +125,33 @@ class DropdownControllerTest { assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); } + @Test + void getStoreEmployeesReturnsAllStaffWhenStoreIdIsNull() { + StoreLocation store = new StoreLocation(); + store.setStoreId(1L); + + Employee staffEmployee = new Employee(); + staffEmployee.setEmployeeId(7L); + staffEmployee.setUserId(7L); + staffEmployee.setFirstName("Alex"); + staffEmployee.setLastName("Jones"); + staffEmployee.setIsActive(true); + + User staffUser = new User(); + staffUser.setId(7L); + staffUser.setRole(User.Role.STAFF); + staffUser.setActive(true); + + when(employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc()) + .thenReturn(List.of(new EmployeeStore(staffEmployee, store))); + when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); + + var response = controller.getStoreEmployees(null); + + assertEquals(1, response.getBody().size()); + assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); + } + @Test void getStoreEmployeesExcludesInactiveStaffUsers() { StoreLocation store = new StoreLocation(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java index 05b2785c..127d75db 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -121,4 +121,12 @@ public class DropdownApi { } return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); } + + public List getEmployees() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/employees"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from all employees endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index 3a331711..56f682c6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -95,7 +95,12 @@ public class AdoptionDialogController { new Thread(() -> { try { Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 ? DropdownApi.getInstance().getStoreEmployees(storeId) : List.of(); + List employees; + if (storeId != null && storeId > 0) { + employees = DropdownApi.getInstance().getStoreEmployees(storeId); + } else { + employees = DropdownApi.getInstance().getEmployees(); + } Platform.runLater(() -> { cbEmployee.setItems(FXCollections.observableArrayList(employees)); applySelectedEmployee(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 472ca7b4..69cfd412 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -427,9 +427,12 @@ public class AppointmentDialogController { new Thread(() -> { try { Long storeId = UserSession.getInstance().getStoreId(); - List employees = storeId != null && storeId > 0 - ? DropdownApi.getInstance().getStoreEmployees(storeId) - : List.of(); + List employees; + if (storeId != null && storeId > 0) { + employees = DropdownApi.getInstance().getStoreEmployees(storeId); + } else { + employees = DropdownApi.getInstance().getEmployees(); + } Platform.runLater(() -> { cbEmployee.setItems(FXCollections.observableArrayList(employees)); applySelectedEmployee(); From 9ea5efe44ebea2fda7e2a524e04beb64063bbf66 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 00:18:49 -0600 Subject: [PATCH 069/137] Fix employee time conflicts --- .../repository/AppointmentRepository.java | 3 ++ .../backend/service/AppointmentService.java | 45 ++++++++----------- .../V17__normalize_appointment_pets.sql | 28 ++++++++++++ .../service/AppointmentServiceTest.java | 41 +++++++++-------- 4 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 5c7b6ec0..2a78244b 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -36,4 +36,7 @@ public interface AppointmentRepository extends JpaRepository "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 8c0e05d8..310c539d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -110,14 +110,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId(), authenticatedUser); - validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), null); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); if (!hasPetIds && !hasCustomerPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); } @@ -125,6 +121,9 @@ public class AppointmentService { Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); + validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); + Appointment appointment = new Appointment(); appointment.setCustomer(customer); appointment.setStore(store); @@ -158,14 +157,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - validateStoreAccess(store.getStoreId(), authenticatedUser); - validateAvailability(store, service, request.getAppointmentDate(), request.getAppointmentTime(), id); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); if (!hasPetIds && !hasCustomerPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); } @@ -173,6 +168,9 @@ public class AppointmentService { Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + validateStoreAccess(store.getStoreId(), authenticatedUser); + validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id); + appointment.setCustomer(customer); appointment.setStore(store); appointment.setService(service); @@ -201,7 +199,6 @@ public class AppointmentService { } @Transactional(readOnly = true) - public List checkAvailability(Long storeId, Long serviceId, LocalDate date) { storeRepository.findById(storeId) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); @@ -209,16 +206,10 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId)); - - //-------------------------------------------------------------------------- - // CHANGED: filter by serviceId too - List existingAppointments = appointmentRepository - .findByStoreAndDate(storeId, date) - .stream() - .filter(a -> a.getService().getServiceId().equals(serviceId)) + List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() + .filter(es -> isAssignableEmployee(es.getEmployee())) + .map(EmployeeStore::getEmployee) .collect(Collectors.toList()); - // ------------------------------------------------------- - List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); @@ -227,7 +218,13 @@ public class AppointmentService { LocalTime currentTime = startTime; while (!currentTime.isAfter(latestStart)) { - if (isSlotAvailable(existingAppointments, service, currentTime, null)) { + final LocalTime slotTime = currentTime; + boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { + List empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); + return isSlotAvailable(empAppointments, service, slotTime, null); + }); + + if (anyEmployeeAvailable) { availableSlots.add(currentTime.toString()); } currentTime = currentTime.plusMinutes(30); @@ -343,15 +340,11 @@ public class AppointmentService { } //------------------------------------ - private void validateAvailability(StoreLocation store, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { - // Filter by same service only - different services can run at same time + private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List existingAppointments = appointmentRepository - .findByStoreAndDate(store.getStoreId(), date) - .stream() - .filter(a -> a.getService().getServiceId().equals(service.getServiceId())) - .collect(Collectors.toList()); + .findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date); if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { - throw new IllegalArgumentException("Appointment time is not available for the selected store and service"); + throw new IllegalArgumentException("The selected employee is already booked for this time slot"); } } diff --git a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql new file mode 100644 index 00000000..00d34751 --- /dev/null +++ b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql @@ -0,0 +1,28 @@ +-- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet + +-- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet +-- Note: pet species and breed might be null in pet table, but we copy them over if present +INSERT INTO customer_pet (customer_id, pet_name, species, breed) +SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed +FROM appointmentPet ap +JOIN appointment a ON a.appointmentId = ap.appointmentId +JOIN pet p ON p.petId = ap.petId +WHERE NOT EXISTS ( + SELECT 1 FROM customer_pet cp + WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName +); + +-- Step 2: Link the appointment to the customer_pet +INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) +SELECT ap.appointmentId, cp.customer_pet_id +FROM appointmentPet ap +JOIN appointment a ON a.appointmentId = ap.appointmentId +JOIN pet p ON p.petId = ap.petId +JOIN customer_pet cp ON cp.customer_id = a.customerId AND cp.pet_name = p.petName +WHERE NOT EXISTS ( + SELECT 1 FROM appointment_customer_pet acp + WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id +); + +-- Step 3: Remove the old legacy relationships so it strictly uses the new ones +DELETE FROM appointmentPet; diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java index aecb5644..3e6f2d89 100644 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java @@ -130,29 +130,34 @@ class AppointmentServiceTest { } @Test - void checkAvailabilityAllowsDifferentServicesAtSameTime() { + void checkAvailabilityAllowsConcurrentAppointmentsIfAnotherEmployeeFree() { + Employee employee2 = new Employee(); + employee2.setEmployeeId(8L); + employee2.setUserId(8L); + employee2.setFirstName("Bob"); + employee2.setIsActive(true); + + User staffUser2 = new User(); + staffUser2.setId(8L); + staffUser2.setRole(User.Role.STAFF); + staffUser2.setActive(true); + when(userRepository.findById(8L)).thenReturn(Optional.of(staffUser2)); + Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); + + when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) + .thenReturn(List.of(new EmployeeStore(employee, store), new EmployeeStore(employee2, store))); + + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of(existing)); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(8L, date)).thenReturn(List.of()); List slots = appointmentService.checkAvailability(1L, 2L, date); assertTrue(slots.contains("10:00")); } - @Test - void checkAvailabilityBlocksSameServiceAtSameTime() { - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of(existing)); - - List slots = appointmentService.checkAvailability(1L, 1L, date); - - assertFalse(slots.contains("10:00")); - } - @Test void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { setAuthentication(7L, User.Role.STAFF); @@ -172,7 +177,7 @@ class AppointmentServiceTest { when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); @@ -204,7 +209,7 @@ class AppointmentServiceTest { when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); @@ -245,7 +250,7 @@ class AppointmentServiceTest { when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); @@ -282,7 +287,7 @@ class AppointmentServiceTest { when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee)); - when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of()); + when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) .thenReturn(List.of(new EmployeeStore(employee, store))); From 661c9b006a2e984e89faadfba57e653360e2cc1a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 00:39:37 -0600 Subject: [PATCH 070/137] Add Missed status --- .../backend/repository/AppointmentRepository.java | 4 ++-- .../db/migration/V18__past_appointments_missed.sql | 10 ++++++++++ .../dialogcontrollers/AppointmentDialogController.java | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V18__past_appointments_missed.sql diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 2a78244b..940f7cf2 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -18,7 +18,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a WHERE a.appointmentDate = :date AND a.appointmentTime = :time") List findByDateAndTime(@Param("date") LocalDate date, @Param("time") LocalTime time); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " + @@ -37,6 +37,6 @@ public interface AppointmentRepository extends JpaRepository "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) <> 'cancelled'") + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); } diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql new file mode 100644 index 00000000..1f3a6707 --- /dev/null +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -0,0 +1,10 @@ +-- V18: Normalize past appointments. +-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. + +UPDATE appointment +SET appointmentStatus = 'Missed' +WHERE LOWER(appointmentStatus) = 'booked' + AND ( + appointmentDate < CURRENT_DATE + OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) + ); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index 69cfd412..cac4f47e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -57,7 +57,7 @@ public class AppointmentDialogController { private ObservableList statusList = FXCollections.observableArrayList( - "Booked", "Completed", "Cancelled" + "Booked", "Completed", "Cancelled", "Missed" ); // From 419e5302f690d0094692f0b7da022064959e19af Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 01:51:58 -0600 Subject: [PATCH 071/137] Fix availability checks --- .../repository/AppointmentRepository.java | 3 ++ .../backend/service/AppointmentService.java | 13 ++++++- .../V18__past_appointments_missed.sql | 39 ++++++++++++++++++- .../controller/DropdownControllerTest.java | 21 ---------- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 940f7cf2..f8649671 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -39,4 +39,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); + + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 310c539d..b4e270fa 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -211,6 +211,17 @@ public class AppointmentService { .map(EmployeeStore::getEmployee) .collect(Collectors.toList()); + if (assignableEmployees.isEmpty()) { + return List.of(); + } + + List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); + List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); + + // Group by employee for faster lookup in the loop + java.util.Map> appointmentsByEmployee = allAppointments.stream() + .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); + List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); LocalTime endTime = LocalTime.of(17, 0); @@ -220,7 +231,7 @@ public class AppointmentService { while (!currentTime.isAfter(latestStart)) { final LocalTime slotTime = currentTime; boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { - List empAppointments = appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(emp.getEmployeeId(), date); + List empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of()); return isSlotAvailable(empAppointments, service, slotTime, null); }); diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql index 1f3a6707..043cad4e 100644 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -1,6 +1,7 @@ --- V18: Normalize past appointments. --- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. +-- V18: Normalize past appointments and resolve initial employee double-bookings +-- Part 1: Normalize past appointments. +-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. UPDATE appointment SET appointmentStatus = 'Missed' WHERE LOWER(appointmentStatus) = 'booked' @@ -8,3 +9,37 @@ WHERE LOWER(appointmentStatus) = 'booked' appointmentDate < CURRENT_DATE OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) ); + +-- Part 2: Resolve potential double-bookings caused by V15's simple backfill. +-- We try to spread overlapping appointments among other active staff in the same store. +-- This is a one-time cleanup for demo data integrity. + +-- Temporary table to find conflicts (same employee, same date, overlapping time) +-- For simplicity in SQL, we just check exact same time for the demo data cleanup. +UPDATE appointment a1 +SET a1.employeeId = ( + SELECT es.employeeId + FROM employeeStore es + JOIN employee e ON e.employeeId = es.employeeId + JOIN users u ON u.id = e.user_id + WHERE es.storeId = a1.storeId + AND e.isActive = TRUE + AND u.role = 'STAFF' + -- Find an employee who DOES NOT have an appointment at this exact time + AND NOT EXISTS ( + SELECT 1 FROM appointment a2 + WHERE a2.employeeId = es.employeeId + AND a2.appointmentDate = a1.appointmentDate + AND a2.appointmentTime = a1.appointmentTime + AND a2.appointmentId <> a1.appointmentId + ) + ORDER BY es.employeeId ASC + LIMIT 1 +) +WHERE EXISTS ( + SELECT 1 FROM appointment a3 + WHERE a3.employeeId = a1.employeeId + AND a3.appointmentDate = a1.appointmentDate + AND a3.appointmentTime = a1.appointmentTime + AND a3.appointmentId < a1.appointmentId +) AND LOWER(a1.appointmentStatus) NOT IN ('cancelled', 'missed'); diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java index e62430e6..e7caa86e 100644 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java @@ -198,25 +198,4 @@ class DropdownControllerTest { assertEquals(1, response.getBody().size()); assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); } - - @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPetsForAdmin() { - User adminUser = new User(); - adminUser.setId(88L); - adminUser.setRole(User.Role.ADMIN); - when(userRepository.findById(88L)).thenReturn(Optional.of(adminUser)); - setAuthentication(88L, User.Role.ADMIN); - - Customer one = new Customer(); - one.setCustomerId(1L); - one.setFirstName("Alex"); - one.setLastName("Brown"); - - when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); - - var response = controller.getAppointmentCustomers(); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - } } From 23d765c6b565da25efe56f80662692e42a48e492 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:12:42 -0600 Subject: [PATCH 072/137] changed detailed fragment to fill data from the backend --- android/app/build.gradle.kts | 36 +-- .../adapters/MessageAdapter.java | 69 ++++- .../petstoremobile/api/MessageApi.java | 12 + .../api/ProductSupplierApi.java | 5 + .../fragments/ChatFragment.java | 269 ++++++++++-------- .../fragments/ProfileFragment.java | 103 ++++--- .../listfragments/AdoptionFragment.java | 4 - .../listfragments/AppointmentFragment.java | 61 ---- .../listfragments/InventoryFragment.java | 4 - .../fragments/listfragments/PetFragment.java | 13 +- .../listfragments/ProductFragment.java | 4 - .../ProductSupplierFragment.java | 3 - .../listfragments/PurchaseOrderFragment.java | 3 - .../listfragments/ServiceFragment.java | 9 +- .../listfragments/SupplierFragment.java | 10 +- .../AdoptionDetailFragment.java | 73 +++-- .../AppointmentDetailFragment.java | 139 ++++++--- .../InventoryDetailFragment.java | 84 ++++-- .../detailfragments/PetDetailFragment.java | 51 +++- .../ProductDetailFragment.java | 68 +++-- .../ProductSupplierDetailFragment.java | 44 ++- .../PurchaseOrderDetailFragment.java | 79 +++-- .../detailfragments/RefundDetailFragment.java | 8 +- .../ServiceDetailFragment.java | 46 ++- .../SupplierDetailFragment.java | 49 +++- .../PetProfileFragment.java | 134 +++++---- .../repositories/AdoptionRepository.java | 26 +- .../repositories/AppointmentRepository.java | 26 +- .../repositories/AuthRepository.java | 25 +- .../repositories/BaseRepository.java | 29 ++ .../repositories/CategoryRepository.java | 10 +- .../repositories/ChatRepository.java | 74 +++++ .../repositories/CustomerRepository.java | 14 +- .../repositories/InventoryRepository.java | 32 +-- .../repositories/PetRepository.java | 34 +-- .../repositories/ProductRepository.java | 34 +-- .../ProductSupplierRepository.java | 29 +- .../repositories/PurchaseOrderRepository.java | 14 +- .../repositories/ServiceRepository.java | 26 +- .../repositories/StoreRepository.java | 10 +- .../repositories/SupplierRepository.java | 26 +- .../petstoremobile/utils/ErrorUtils.java | 43 ++- .../petstoremobile/utils/RetrofitUtils.java | 14 +- .../viewmodels/ChatViewModel.java | 68 +++++ android/gradle/libs.versions.toml | 24 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- 46 files changed, 1181 insertions(+), 760 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 6cd974d9..caaf8e17 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -58,41 +58,45 @@ android { } dependencies { + // Core AndroidX & UI implementation(libs.appcompat) implementation(libs.material) implementation(libs.activity) implementation(libs.constraintlayout) + implementation(libs.swiperefreshlayout) + implementation(libs.viewpager2) + // Hilt Dependency Injection implementation(libs.hilt.android) annotationProcessor(libs.hilt.compiler) + // Navigation Component implementation(libs.navigation.fragment) implementation(libs.navigation.ui) - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") + // Networking + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) - implementation("com.google.android.material:material:1.11.0") - implementation("androidx.viewpager2:viewpager2:1.1.0") + // CameraX + implementation(libs.camera.core) + implementation(libs.camera.camera2) + implementation(libs.camera.lifecycle) + implementation(libs.camera.view) - implementation("androidx.camera:camera-core:1.4.0") - implementation("androidx.camera:camera-camera2:1.4.0") - implementation("androidx.camera:camera-lifecycle:1.4.0") - implementation("androidx.camera:camera-view:1.4.0") - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") - implementation(libs.swiperefreshlayout) + // Image Loading + implementation(libs.glide) + annotationProcessor(libs.glide.compiler) + // Other Third-party Libraries implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6") implementation("io.reactivex.rxjava2:rxjava:2.2.21") implementation("io.reactivex.rxjava2:rxandroid:2.1.1") - - implementation("com.github.bumptech.glide:glide:4.16.0") - annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") - implementation("com.github.prolificinteractive:material-calendarview:2.0.1") + // Testing testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index 0354a1fd..de6ccc04 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -3,9 +3,14 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.models.Message; import java.util.List; @@ -17,6 +22,7 @@ public class MessageAdapter extends RecyclerView.Adapter messages; private Long currentUserId; + private String token; public MessageAdapter(List messages, Long currentUserId) { this.messages = messages; @@ -28,6 +34,10 @@ public class MessageAdapter extends RecyclerView.Adapter sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); + + @Multipart + @POST("api/v1/chat/conversations/{id}/messages/attachment") + Call sendMessageWithAttachment( + @Path("id") Long conversationId, + @Part("content") RequestBody content, + @Part MultipartBody.Part file + ); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index 32810b12..7ec9eb07 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -12,6 +12,11 @@ public interface ProductSupplierApi { @Query("page") int page, @Query("size") int size); + @GET("api/v1/product-suppliers/{productId}/{supplierId}") + Call getProductSupplierById( + @Path("productId") Long productId, + @Path("supplierId") Long supplierId); + @POST("api/v1/product-suppliers") Call createProductSupplier(@Body ProductSupplierDTO dto); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java index 81383acc..5ed6ac6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -14,33 +14,35 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.core.view.GravityCompat; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import com.bumptech.glide.Glide; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.MessageAdapter; import com.example.petstoremobile.api.auth.TokenManager; -import com.example.petstoremobile.api.ChatApi; -import com.example.petstoremobile.api.CustomerApi; -import com.example.petstoremobile.api.MessageApi; import com.example.petstoremobile.databinding.FragmentChatBinding; import com.example.petstoremobile.dtos.ConversationDTO; -import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.MessageDTO; import com.example.petstoremobile.dtos.SendMessageRequest; import com.example.petstoremobile.models.Chat; import com.example.petstoremobile.models.Message; import com.example.petstoremobile.services.ChatNotificationService; -import com.example.petstoremobile.utils.RetrofitUtils; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.ChatViewModel; import com.example.petstoremobile.websocket.StompChatManager; +import java.io.File; import java.util.*; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; @AndroidEntryPoint public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, @@ -49,6 +51,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private static final String TAG = "ChatFragment"; private FragmentChatBinding binding; + private ChatViewModel viewModel; // Adapters private ChatAdapter chatAdapter; @@ -60,10 +63,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis private final Map customerNames = new HashMap<>(); private Uri pendingAttachmentUri; - // APIs - @Inject ChatApi chatApi; - @Inject CustomerApi customerApi; - @Inject MessageApi messageApi; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; @@ -79,6 +78,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(ChatViewModel.class); attachmentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -183,54 +183,53 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Fetches a list of customers from the API to display customer names for the chat list. + * Fetches a list of customers from the ViewModel to display customer names for the chat list. */ private void loadCustomers() { - customerApi.getAllCustomers(0, 100).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - for (CustomerDTO c : result.getContent()) { - customerNames.put(c.getCustomerId(), c.getFullName()); + viewModel.getAllCustomers(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + resource.data.getContent().forEach(c -> customerNames.put(c.getCustomerId(), c.getFullName())); + loadConversations(); } - loadConversations(); - })); + }); } /** - * Retrieves all conversations for the current user and populates the chat drawer. + * Retrieves all conversations for the current user through the ViewModel and populates the chat drawer. */ private void loadConversations() { - chatApi.getAllConversations().enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - chatList.clear(); - List loaded = result.stream() - .map(dto -> { - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - return new Chat(String.valueOf(dto.getId()), - name, dto.getLastMessage(), - dto.getCustomerId(), dto.getStaffId()); - }) - .collect(Collectors.toList()); - chatList.addAll(loaded); - chatAdapter.notifyDataSetChanged(); - - if (activeConversationId != null) { - setConversationActive(true); - // Update title to customer name of active conversation - for (Chat chat : chatList) { - if (chat.getChatId().equals(String.valueOf(activeConversationId))) { - binding.tvChatTitle.setText(chat.getCustomerName()); - break; + viewModel.getAllConversations().observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + chatList.clear(); + for (ConversationDTO dto : resource.data) { + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + chatList.add(new Chat(String.valueOf(dto.getId()), + name, dto.getLastMessage(), + dto.getCustomerId(), dto.getStaffId())); + } + chatAdapter.notifyDataSetChanged(); + + if (activeConversationId != null) { + setConversationActive(true); + // Update title to customer name of active conversation + for (Chat chat : chatList) { + if (chat.getChatId().equals(String.valueOf(activeConversationId))) { + binding.tvChatTitle.setText(chat.getCustomerName()); + break; + } } + if (stompChatManager != null) { + stompChatManager.subscribeToConversation(activeConversationId); + } + loadMessageHistory(activeConversationId); + } else { + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + setConversationActive(false); } - if (stompChatManager != null) { - stompChatManager.subscribeToConversation(activeConversationId); - } - loadMessageHistory(activeConversationId); - } else { - messageList.clear(); - messageAdapter.notifyDataSetChanged(); - setConversationActive(false); } - })); + }); } /** @@ -251,21 +250,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Fetches the full message history for a specific conversation from the API. + * Fetches the full message history for a specific conversation from the ViewModel. */ private void loadMessageHistory(Long conversationId) { - messageApi.getMessages(conversationId).enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - messageList.clear(); - for (MessageDTO dto : result) { - messageList.add(dtoToModel(dto)); + viewModel.getMessages(conversationId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + messageList.clear(); + for (MessageDTO dto : resource.data) { + messageList.add(dtoToModel(dto)); + } + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); } - messageAdapter.notifyDataSetChanged(); - scrollToBottom(); - })); + }); } /** - * Sends a plain text message to the currently active conversation. + * Sends a plain text message to the currently active conversation through the ViewModel. */ private void sendMessage() { //check if a chat is selected @@ -278,14 +279,15 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //clear text field after sending binding.etMessage.setText(""); - //calls api to send the message - messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) - .enqueue(RetrofitUtils.createSilentCallback(TAG, result -> { - messageList.add(dtoToModel(result)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); - loadConversations(); - })); + //calls viewmodel to send the message + viewModel.sendMessage(activeConversationId, new SendMessageRequest(text)).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + messageList.add(dtoToModel(resource.data)); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + loadConversations(); + } + }); } /** @@ -351,13 +353,34 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } /** - * Handles sending a message that includes a file attachment. + * Handles sending a message that includes a file attachment via the ViewModel. */ private void sendWithAttachment(Uri uri) { if (activeConversationId == null) return; + String text = binding.etMessage.getText().toString().trim(); + binding.etMessage.setText(""); + removeAttachment(); - //TODO: send the message with attachment when backend is done - Log.d(TAG, "Send with attachment happening"); + try { + File file = FileUtils.getFileFromUri(requireContext(), uri); + if (file == null) return; + + String mimeType = requireContext().getContentResolver().getType(uri); + RequestBody requestFile = RequestBody.create(file, MediaType.parse(mimeType != null ? mimeType : "application/octet-stream")); + MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), requestFile); + RequestBody contentPart = RequestBody.create(text, MediaType.parse("text/plain")); + + viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + messageList.add(dtoToModel(resource.data)); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + loadConversations(); + } + }); + } catch (Exception e) { + Log.e(TAG, "Error sending message with attachment", e); + } } /** @@ -376,8 +399,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis //else add the message to the active chat if it's not from the current user messageList.add(dtoToModel(dto)); - messageAdapter.notifyItemInserted(messageList.size() - 1); - scrollToBottom(); + requireActivity().runOnUiThread(() -> { + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + }); } /** @@ -385,41 +410,43 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis */ @Override public void onConversationUpdated(ConversationDTO dto) { - boolean updated = false; - String name = customerNames.getOrDefault( - dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + requireActivity().runOnUiThread(() -> { + boolean updated = false; + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(dto.getId()))) { - chatList.set(i, new Chat( + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(dto.getId()))) { + chatList.set(i, new Chat( + String.valueOf(dto.getId()), + name, + dto.getLastMessage(), + dto.getCustomerId(), + dto.getStaffId() + )); + chatAdapter.notifyItemChanged(i); + updated = true; + break; + } + } + + if (!updated) { + chatList.add(0, new Chat( String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId() )); - chatAdapter.notifyItemChanged(i); - updated = true; - break; + chatAdapter.notifyItemInserted(0); } - } - if (!updated) { - chatList.add(0, new Chat( - String.valueOf(dto.getId()), - name, - dto.getLastMessage(), - dto.getCustomerId(), - dto.getStaffId() - )); - chatAdapter.notifyItemInserted(0); - } - - if (activeConversationId != null && activeConversationId.equals(dto.getId())) { - setConversationActive(true); - binding.tvChatTitle.setText(name); - } + if (activeConversationId != null && activeConversationId.equals(dto.getId())) { + setConversationActive(true); + binding.tvChatTitle.setText(name); + } + }); } /** @@ -430,10 +457,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + requireActivity().runOnUiThread(() -> { + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + }); } /** @@ -444,7 +473,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); + requireActivity().runOnUiThread(this::loadConversations); } /** @@ -455,10 +484,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (!isAdded()) { return; } - loadConversations(); - if (activeConversationId != null) { - loadMessageHistory(activeConversationId); - } + requireActivity().runOnUiThread(() -> { + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + }); } /** @@ -495,21 +526,23 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis if (conversationId == null) { return; } - for (int i = 0; i < chatList.size(); i++) { - Chat existing = chatList.get(i); - if (existing.getChatId().equals(String.valueOf(conversationId))) { - Chat updated = new Chat( - existing.getChatId(), - existing.getCustomerName(), - lastMessage, - existing.getCustomerId(), - existing.getStaffId() - ); - chatList.set(i, updated); - chatAdapter.notifyItemChanged(i); - return; + requireActivity().runOnUiThread(() -> { + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(conversationId))) { + Chat updated = new Chat( + existing.getChatId(), + existing.getCustomerName(), + lastMessage, + existing.getCustomerId(), + existing.getStaffId() + ); + chatList.set(i, updated); + chatAdapter.notifyItemChanged(i); + return; + } } - } + }); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index 9d8a472f..c80a82ae 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -4,8 +4,10 @@ import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import android.util.Log; import android.view.LayoutInflater; @@ -25,8 +27,9 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; -import com.example.petstoremobile.utils.RetrofitUtils; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.UIUtils; +import com.example.petstoremobile.viewmodels.AuthViewModel; import java.io.File; import java.util.HashMap; @@ -48,9 +51,9 @@ public class ProfileFragment extends Fragment { private FragmentProfileBinding binding; private UserDTO currentUser; + private AuthViewModel viewModel; private boolean hasImage = false; - @Inject AuthApi authApi; @Inject TokenManager tokenManager; @Inject @Named("baseUrl") String baseUrl; @@ -62,6 +65,7 @@ public class ProfileFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(AuthViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -178,31 +182,36 @@ public class ProfileFragment extends Fragment { * Fetches current user profile data from the API and then updates the UI. */ private void loadProfileData() { - authApi.getMe().enqueue(RetrofitUtils.createCallback(requireContext(), "PROFILE", null, result -> { - currentUser = result; + viewModel.getMe().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + currentUser = resource.data; - //set the user data to the view - binding.tvProfileName.setText(currentUser.getFullName()); - binding.tvProfileEmail.setText(currentUser.getEmail()); - binding.tvProfilePhone.setText(currentUser.getPhone()); - binding.tvProfileRole.setText(currentUser.getRole()); + //set the user data to the view + binding.tvProfileName.setText(currentUser.getFullName()); + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); + binding.tvProfileRole.setText(currentUser.getRole()); - // get the avatar endpoint to load profile image and the token for authorization - String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; - String token = tokenManager.getToken(); + // get the avatar endpoint to load profile image and the token for authorization + String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH; + String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { - @Override - public void onResourceReady() { - hasImage = true; - } + GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } - @Override - public void onLoadFailed() { - hasImage = false; - } - }); - })); + @Override + public void onLoadFailed() { + hasImage = false; + } + }); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -218,11 +227,17 @@ public class ProfileFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); //Call the backend to upload the avatar - authApi.uploadAvatar(body).enqueue(RetrofitUtils.createCallback(requireContext(), "UPLOAD_AVATAR", "Avatar updated successfully", result -> { - currentUser = result; - // Reload image after successful upload - loadProfileData(); - })); + viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS) { + currentUser = resource.data; + Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); + // Reload image after successful upload + loadProfileData(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } catch (Exception e) { Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage()); } @@ -232,10 +247,16 @@ public class ProfileFragment extends Fragment { * Sends a request to the API to delete the current user's avatar image. */ private void deleteAvatar() { - authApi.deleteAvatar().enqueue(RetrofitUtils.createCallback(requireContext(), "DELETE_AVATAR", "Avatar removed successfully", result -> { - hasImage = false; - binding.imgProfile.setImageResource(R.drawable.placeholder); - })); + viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS) { + hasImage = false; + binding.imgProfile.setImageResource(R.drawable.placeholder); + Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } /** @@ -245,11 +266,17 @@ public class ProfileFragment extends Fragment { Map updates = new HashMap<>(); updates.put(fieldName, value); - authApi.updateMe(updates).enqueue(RetrofitUtils.createCallback(requireContext(), "UPDATE_PROFILE", "Profile updated successfully", result -> { - currentUser = result; - // Update the view with the new data from backend - binding.tvProfileEmail.setText(currentUser.getEmail()); - binding.tvProfilePhone.setText(currentUser.getPhone()); - })); + viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + currentUser = resource.data; + Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show(); + // Update the view with the new data from backend + binding.tvProfileEmail.setText(currentUser.getEmail()); + binding.tvProfilePhone.setText(currentUser.getPhone()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java index 04713d86..fafd0d93 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -239,10 +239,6 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop if (position != -1) { AdoptionDTO a = filteredList.get(position); args.putLong("adoptionId", a.getAdoptionId()); - args.putLong("petId", a.getPetId() != null ? a.getPetId() : -1); - args.putLong("customerId", a.getCustomerId() != null ? a.getCustomerId() : -1); - args.putString("adoptionDate", a.getAdoptionDate()); - args.putString("adoptionStatus", a.getAdoptionStatus()); } NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index fb1455ae..c93b9e89 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -22,13 +22,8 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; -import com.example.petstoremobile.dtos.ServiceDTO; -import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.AppointmentViewModel; -import com.example.petstoremobile.viewmodels.PetViewModel; -import com.example.petstoremobile.viewmodels.ServiceViewModel; -import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.EventDecorator; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -50,13 +45,9 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); private List filteredList = new ArrayList<>(); - private List petList = new ArrayList<>(); - private List serviceList = new ArrayList<>(); private AppointmentAdapter adapter; private AppointmentViewModel appointmentViewModel; - private PetViewModel petViewModel; - private ServiceViewModel serviceViewModel; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; @@ -69,8 +60,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); - petViewModel = new ViewModelProvider(this).get(PetViewModel.class); - serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); } /** @@ -86,8 +75,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupSwipeRefresh(); setupCalendar(); loadAppointmentData(); - loadPets(); - loadServices(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -226,37 +213,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void openAppointmentDetails(int position) { Bundle args = new Bundle(); - if (position != -1) { AppointmentDTO a = filteredList.get(position); args.putLong("appointmentId", a.getAppointmentId()); - args.putString("appointmentDate", a.getAppointmentDate()); - args.putString("appointmentTime", a.getAppointmentTime()); - args.putString("appointmentStatus", a.getAppointmentStatus()); - // IDs for pre-selecting spinners - if (a.getPetID() != null) args.putLong("petId", a.getPetID()); - if (a.getServiceId() != null) args.putLong("serviceId", a.getServiceId()); - if (a.getCustomerId() != null) args.putLong("customerId", a.getCustomerId()); - if (a.getStoreId() != null) args.putLong("storeId", a.getStoreId()); } - NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); } - /** - * Reloads data when an appointment is saved. - */ - public void onAppointmentSaved(int position, AppointmentDTO appointment) { - loadAppointmentData(); - } - - /** - * Reloads data when an appointment is deleted. - */ - public void onAppointmentDeleted(int position) { - loadAppointmentData(); - } - /** * Handles item click in the appointment list. */ @@ -299,30 +262,6 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. }); } - /** - * Fetches the full list of pets from the server. - */ - private void loadPets() { - petViewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - petList.clear(); - petList.addAll(resource.data.getContent()); - } - }); - } - - /** - * Fetches the full list of services from the server. - */ - private void loadServices() { - serviceViewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - serviceList.clear(); - serviceList.addAll(resource.data.getContent()); - } - }); - } - /** * Initializes the RecyclerView for displaying appointments. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 07e08aba..029dc446 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -341,10 +341,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn if (inv != null) { args.putLong("inventoryId", inv.getInventoryId()); - args.putLong("prodId", inv.getProdId() != null ? inv.getProdId() : -1); - args.putString("productName", inv.getProductName()); - args.putString("categoryName", inv.getCategoryName()); - args.putInt("quantity", inv.getQuantity() != null ? inv.getQuantity() : 0); } NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 0c71593a..01331b19 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -166,18 +166,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = filteredList.get(position); - args.putInt("petId", pet.getPetId().intValue()); - args.putString("petName", pet.getPetName()); - args.putString("petSpecies", pet.getPetSpecies()); - args.putString("petBreed", pet.getPetBreed()); - args.putInt("petAge", pet.getPetAge()); - args.putString("petStatus", pet.getPetStatus()); - - try { - args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice())); - } catch (Exception e) { - args.putDouble("petPrice", 0.0); - } + args.putLong("petId", pet.getPetId()); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index a459262f..6103918e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -170,10 +170,6 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (position != -1) { ProductDTO p = filteredList.get(position); args.putLong("prodId", p.getProdId()); - args.putString("prodName", p.getProdName()); - args.putString("prodDesc", p.getProdDesc() != null ? p.getProdDesc() : ""); - args.putString("prodPrice", p.getProdPrice() != null ? p.getProdPrice().toString() : ""); - args.putLong("categoryId", p.getCategoryId() != null ? p.getCategoryId() : -1); } NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 49eccc94..4c8e08d0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -166,9 +166,6 @@ public class ProductSupplierFragment extends Fragment ProductSupplierDTO ps = filteredList.get(position); args.putLong("productId", ps.getProductId()); args.putLong("supplierId", ps.getSupplierId()); - args.putString("productName", ps.getProductName()); - args.putString("supplierName", ps.getSupplierName()); - args.putString("cost", ps.getCost() != null ? ps.getCost().toString() : ""); } NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 078ef06c..78fcd981 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -166,9 +166,6 @@ public class PurchaseOrderFragment extends Fragment Bundle args = new Bundle(); PurchaseOrderDTO po = filteredList.get(position); args.putLong("purchaseOrderId", po.getPurchaseOrderId()); - args.putString("supplierName", po.getSupplierName()); - args.putString("orderDate", po.getOrderDate()); - args.putString("status", po.getStatus()); NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index 8ae3c5a5..ff8e1bb5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -128,16 +128,11 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private void openServiceDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - args.putInt("position", position); - //if editing a service, add the service data to the bundle + //if editing a service, add the service id to the bundle if (position != -1) { ServiceDTO service = filteredList.get(position); - args.putInt("serviceId", service.getServiceId().intValue()); - args.putString("serviceName", service.getServiceName()); - args.putString("serviceDesc", service.getServiceDesc()); - args.putInt("serviceDuration", service.getServiceDuration()); - args.putDouble("servicePrice", service.getServicePrice()); + args.putLong("serviceId", service.getServiceId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_service_detail, args); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index 4018c27a..e3a0c131 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -129,17 +129,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private void openSupplierDetails(int position) { //Make a bundle to pass data to the detail fragment Bundle args = new Bundle(); - args.putInt("position", position); - //if editing a supplier, add the supplier data to the bundle + //if editing a supplier, add the supplier id to the bundle if (position != -1) { SupplierDTO supplier = filteredList.get(position); - args.putInt("supId", supplier.getSupId().intValue()); - args.putString("supCompany", supplier.getSupCompany()); - args.putString("supContactFirstName", supplier.getSupContactFirstName()); - args.putString("supContactLastName", supplier.getSupContactLastName()); - args.putString("supEmail", supplier.getSupEmail()); - args.putString("supPhone", supplier.getSupPhone()); + args.putLong("supId", supplier.getSupId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_supplier_detail, args); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 3702e168..1bdaba71 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -56,15 +57,20 @@ public class AdoptionDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAdoptionDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadData(); + loadSpinnersData(); handleArguments(); binding.btnAdoptionBack.setOnClickListener(v -> navigateBack()); binding.btnSaveAdoption.setOnClickListener(v -> saveAdoption()); binding.btnDeleteAdoption.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -96,9 +102,9 @@ public class AdoptionDetailFragment extends Fragment { } /** - * Fetches required data (pets and customers) from the backend. + * Fetches required data for spinners from the backend. */ - private void loadData() { + private void loadSpinnersData() { loadPets(); loadCustomers(); } @@ -110,13 +116,20 @@ public class AdoptionDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + refreshPetSpinner(); } }); } + /** + * Populates the pet selection spinner with data. + */ + private void refreshPetSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + /** * Loads the list of customers from the API. */ @@ -124,14 +137,21 @@ public class AdoptionDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + refreshCustomerSpinner(); } }); } + /** + * Populates the customer selection spinner with data. + */ + private void refreshCustomerSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -139,18 +159,12 @@ public class AdoptionDetailFragment extends Fragment { Bundle a = getArguments(); if (a != null && a.containsKey("adoptionId")) { isEditing = true; - adoptionId = a.getLong("adoptionId"); - preselectedPetId = a.getLong("petId", -1); - preselectedCustomerId = a.getLong("customerId", -1); - + adoptionId = a.getLong("adoptionId"); binding.tvAdoptionMode.setText("Edit Adoption"); binding.tvAdoptionId.setText("ID: " + adoptionId); binding.tvAdoptionId.setVisibility(View.VISIBLE); - binding.etAdoptionDate.setText(a.getString("adoptionDate")); binding.btnDeleteAdoption.setVisibility(View.VISIBLE); - - // Pre-fill status - SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getString("adoptionStatus", "Pending")); + loadAdoptionData(); } else { binding.tvAdoptionMode.setText("Add Adoption"); binding.btnDeleteAdoption.setVisibility(View.GONE); @@ -158,6 +172,27 @@ public class AdoptionDetailFragment extends Fragment { } } + /** + * Fetches specific adoption details from the backend using the ID. + */ + private void loadAdoptionData() { + adoptionViewModel.getAdoptionById(adoptionId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AdoptionDTO a = resource.data; + preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; + preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + binding.etAdoptionDate.setText(a.getAdoptionDate()); + SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); + + refreshPetSpinner(); + refreshCustomerSpinner(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Validates input and saves the adoption request to the backend. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 83e70852..cc419a69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -68,15 +69,20 @@ public class AppointmentDetailFragment extends Fragment { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentAppointmentDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinners(); setupDatePicker(); - loadData(); + loadSpinnersData(); handleArguments(); binding.btnApptBack.setOnClickListener(v -> navigateBack()); binding.btnSaveAppointment.setOnClickListener(v -> saveAppointment()); binding.btnDeleteAppointment.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -115,9 +121,9 @@ public class AppointmentDetailFragment extends Fragment { } /** - * Fetches all required data from the backend to populate the fragment. + * Fetches all required data for spinners from the backend. */ - private void loadData() { + private void loadSpinnersData() { loadPets(); loadServices(); loadCustomers(); @@ -131,13 +137,20 @@ public class AppointmentDetailFragment extends Fragment { petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, - PetDTO::getPetName, "-- Select Pet --", - preselectedPetId, PetDTO::getPetId); + refreshPetSpinner(); } }); } + /** + * Populates the pet selection spinner. + */ + private void refreshPetSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPet, petList, + PetDTO::getPetName, "-- Select Pet --", + preselectedPetId, PetDTO::getPetId); + } + /** * Loads the list of services from the API. */ @@ -145,13 +158,20 @@ public class AppointmentDetailFragment extends Fragment { serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { serviceList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, - ServiceDTO::getServiceName, "-- Select Service --", - preselectedServiceId, ServiceDTO::getServiceId); + refreshServiceSpinner(); } }); } + /** + * Populates the service selection spinner. + */ + private void refreshServiceSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerService, serviceList, + ServiceDTO::getServiceName, "-- Select Service --", + preselectedServiceId, ServiceDTO::getServiceId); + } + /** * Loads the list of customers from the API. */ @@ -159,14 +179,21 @@ public class AppointmentDetailFragment extends Fragment { customerViewModel.getAllCustomers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { customerList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, - item -> item.getFirstName() + " " + item.getLastName(), - "-- Select Customer --", - preselectedCustomerId, CustomerDTO::getCustomerId); + refreshCustomerSpinner(); } }); } + /** + * Populates the customer selection spinner. + */ + private void refreshCustomerSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerCustomer, customerList, + item -> item.getFirstName() + " " + item.getLastName(), + "-- Select Customer --", + preselectedCustomerId, CustomerDTO::getCustomerId); + } + /** * Loads the list of stores from the API. */ @@ -174,13 +201,20 @@ public class AppointmentDetailFragment extends Fragment { storeViewModel.getAllStores(0, 50).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { storeList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, - StoreDTO::getStoreName, "-- Select Store --", - preselectedStoreId, StoreDTO::getStoreId); + refreshStoreSpinner(); } }); } + /** + * Populates the store selection spinner. + */ + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -188,34 +222,12 @@ public class AppointmentDetailFragment extends Fragment { Bundle a = getArguments(); if (a != null && a.containsKey("appointmentId")) { isEditing = true; - appointmentId = a.getLong("appointmentId"); - preselectedPetId = a.getLong("petId", -1); - preselectedServiceId= a.getLong("serviceId", -1); - preselectedCustomerId = a.getLong("customerId", -1); - preselectedStoreId = a.getLong("storeId", -1); - + appointmentId = a.getLong("appointmentId"); binding.tvApptMode.setText("Edit Appointment"); binding.tvAppointmentId.setText("ID: " + appointmentId); binding.tvAppointmentId.setVisibility(View.VISIBLE); - binding.etAppointmentDate.setText(a.getString("appointmentDate")); binding.btnDeleteAppointment.setVisibility(View.VISIBLE); - - // Pre-fill time spinners - String time = a.getString("appointmentTime", "09:00"); - if (time.length() > 5) time = time.substring(0, 5); - String[] parts = time.split(":"); - if (parts.length == 2) { - int hour = Integer.parseInt(parts[0]); - int min = Integer.parseInt(parts[1]); - for (int i = 0; i < HOURS.length; i++) - if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; } - for (int i = 0; i < MINUTES.length; i++) - if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; } - } - - // Pre-fill status - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getString("appointmentStatus", "Booked")); - + loadAppointmentData(); } else { binding.tvApptMode.setText("Add Appointment"); binding.btnDeleteAppointment.setVisibility(View.GONE); @@ -223,6 +235,48 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Fetches specific appointment details from the backend using the ID. + */ + private void loadAppointmentData() { + appointmentViewModel.getAppointmentById(appointmentId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + AppointmentDTO a = resource.data; + preselectedPetId = (a.getPetID() != null) ? a.getPetID() : -1; + preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; + preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; + preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; + + binding.etAppointmentDate.setText(a.getAppointmentDate()); + + // Pre-fill time spinners + String time = a.getAppointmentTime() != null ? a.getAppointmentTime() : "09:00"; + if (time.length() > 5) time = time.substring(0, 5); + String[] parts = time.split(":"); + if (parts.length == 2) { + try { + int hour = Integer.parseInt(parts[0]); + int min = Integer.parseInt(parts[1]); + for (int i = 0; i < HOURS.length; i++) + if (HOURS[i] == hour) { binding.spinnerHour.setSelection(i); break; } + for (int i = 0; i < MINUTES.length; i++) + if (MINUTES[i] == min) { binding.spinnerMinute.setSelection(i); break; } + } catch (NumberFormatException ignored) {} + } + + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getAppointmentStatus()); + + refreshPetSpinner(); + refreshServiceSpinner(); + refreshCustomerSpinner(); + refreshStoreSpinner(); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Validates input and saves the appointment to the backend. */ @@ -307,6 +361,9 @@ public class AppointmentDetailFragment extends Fragment { } } + /** + * Handles errors that occur during the saving process. + */ private void handleSaveError(String errorMessage) { if (errorMessage != null) { Log.e("APPT_SAVE", "Error: " + errorMessage); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index bafa71c5..cd1e84f4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -12,6 +12,7 @@ import android.widget.ArrayAdapter; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -19,6 +20,7 @@ import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; +import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.InputValidator; @@ -56,6 +58,9 @@ public class InventoryDetailFragment extends Fragment { private final List productSuggestions = new ArrayList<>(); private ArrayAdapter dropdownAdapter; + /** + * Initializes the view models. + */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,10 +68,22 @@ public class InventoryDetailFragment extends Fragment { productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); } + /** + * Inflates the layout. + */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentInventoryDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + /** + * Sets up UI components after the view is created. + */ + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupProductSearch(); handleArguments(); @@ -80,27 +97,26 @@ public class InventoryDetailFragment extends Fragment { android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); binding.etProductSearch.setAdapter(dropdownAdapter); binding.etProductSearch.setThreshold(1); // start showing after 1 character - - return binding.getRoot(); } @Override public void onDestroyView() { super.onDestroyView(); + if (searchRunnable != null) { + searchHandler.removeCallbacks(searchRunnable); + } binding = null; } /** - * setup the product search dropdown. + * Sets up the product search dropdown. */ private void setupProductSearch() { binding.etProductSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { + @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { } - @Override - public void afterTextChanged(Editable s) { + @Override public void afterTextChanged(Editable s) { } @Override @@ -137,6 +153,7 @@ public class InventoryDetailFragment extends Fragment { * Searches for products matching the query from the backend. */ private void searchProducts(String query) { + if (getView() == null) return; productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productSuggestions.clear(); @@ -157,7 +174,7 @@ public class InventoryDetailFragment extends Fragment { } /** - * arguments to set up edit or add mode. + * Handles fragment arguments to determine if we are in edit or add mode. */ private void handleArguments() { Bundle args = getArguments(); @@ -168,27 +185,10 @@ public class InventoryDetailFragment extends Fragment { binding.tvInventoryMode.setText("Edit Inventory"); binding.tvInventoryId.setText("Inventory ID: " + inventoryId); binding.tvInventoryId.setVisibility(View.VISIBLE); - - // Pre-fill search box with existing product name - String productName = args.getString("productName", ""); - long prodId = args.getLong("prodId", -1); - binding.etProductSearch.setText(productName); - - // Show existing product info - if (prodId != -1) { - binding.tvProductInfo.setText( - "ID: " + prodId - + " • " + args.getString("categoryName", "")); - binding.tvProductInfo.setVisibility(View.VISIBLE); - - // Build a minimal ProductDTO so selectedProduct is not null on save - selectedProduct = new ProductDTO(productName, null, null, null); - selectedProduct.setProdId(prodId); - } - - binding.etQuantity.setText(String.valueOf(args.getInt("quantity", 0))); binding.btnDeleteInventory.setVisibility(View.VISIBLE); binding.btnSaveInventory.setText("Save"); + + loadInventoryData(); } else { isEditing = false; binding.tvInventoryMode.setText("Add Inventory"); @@ -200,7 +200,35 @@ public class InventoryDetailFragment extends Fragment { } /** - * Saves the current inventory item details to the backend. + * Loads existing inventory data from the backend. + */ + private void loadInventoryData() { + inventoryViewModel.getInventoryById(inventoryId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + InventoryDTO inv = resource.data; + binding.etProductSearch.setText(inv.getProductName()); + binding.etQuantity.setText(String.valueOf(inv.getQuantity())); + + if (inv.getProdId() != null) { + binding.tvProductInfo.setText( + "ID: " + inv.getProdId() + + " • " + inv.getCategoryName()); + binding.tvProductInfo.setVisibility(View.VISIBLE); + + selectedProduct = new ProductDTO(); + selectedProduct.setProdId(inv.getProdId()); + selectedProduct.setProdName(inv.getProductName()); + selectedProduct.setCategoryName(inv.getCategoryName()); + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Validates input and saves the current inventory item details to the backend. */ private void saveInventory() { if (selectedProduct == null) { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 9af2b79d..eb34869c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -31,7 +32,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; - private int petId; + private long petId; private boolean isEditing = false; private PetViewModel viewModel; @@ -46,6 +47,12 @@ public class PetDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPetDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinner(); handleArguments(); @@ -54,8 +61,6 @@ public class PetDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet()); - - return binding.getRoot(); } @Override @@ -95,10 +100,10 @@ public class PetDetailFragment extends Fragment { //check if the pet is being edited or added if (isEditing) { // Update existing pet - petDTO.setPetId((long) petId); - viewModel.updatePet((long) petId, petDTO).observe(getViewLifecycleOwner(), resource -> { + petDTO.setPetId(petId); + viewModel.updatePet(petId, petDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId); + ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) petId); Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -124,9 +129,9 @@ public class PetDetailFragment extends Fragment { */ private void deletePet() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Pet", () -> - viewModel.deletePet((long) petId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deletePet(petId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId); + ActivityLogger.logChange(requireContext(), "Pet", "DELETED", (int) petId); Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); navigateToPetList(); } else if (resource.status == Resource.Status.ERROR) { @@ -157,16 +162,12 @@ public class PetDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("petId")) { // Get pet data from arguments and populate fields isEditing = true; - petId = getArguments().getInt("petId"); + petId = getArguments().getLong("petId"); binding.tvMode.setText("Edit Pet"); binding.tvPetId.setText("ID: " + petId); - binding.etPetName.setText(getArguments().getString("petName")); - binding.etPetSpecies.setText(getArguments().getString("petSpecies")); - binding.etPetBreed.setText(getArguments().getString("petBreed")); - binding.etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); - binding.etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); - SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, getArguments().getString("petStatus")); + binding.tvPetId.setVisibility(View.VISIBLE); binding.btnDeletePet.setVisibility(View.VISIBLE); + loadPetData(); } else { // Pet is being added // Set default values for add a new pet @@ -178,6 +179,26 @@ public class PetDetailFragment extends Fragment { } } + /** + * Fetches specific pet details from the backend using the ID. + */ + private void loadPetData() { + viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PetDTO p = resource.data; + binding.etPetName.setText(p.getPetName()); + binding.etPetSpecies.setText(p.getPetSpecies()); + binding.etPetBreed.setText(p.getPetBreed()); + binding.etPetAge.setText(String.valueOf(p.getPetAge())); + binding.etPetPrice.setText(p.getPetPrice()); + SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Initializes the spinner for pet status selection. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 4784fd03..48a988d2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -21,6 +22,7 @@ import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; +import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import java.io.File; @@ -89,12 +91,21 @@ public class ProductDetailFragment extends Fragment { } /** - * Inflates the layout and initializes UI components and listeners. + * Inflates the layout. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentProductDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + /** + * Sets up UI components and listeners after the view is created. + */ + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); loadCategories(); handleArguments(); @@ -103,7 +114,6 @@ public class ProductDetailFragment extends Fragment { binding.btnSaveProduct.setOnClickListener(v -> saveProduct()); binding.btnDeleteProduct.setOnClickListener(v -> confirmDelete()); binding.ivProductImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Product Image", hasImage)); - return binding.getRoot(); } @Override @@ -117,7 +127,7 @@ public class ProductDetailFragment extends Fragment { */ private void loadCategories() { viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { categoryList = resource.data.getContent(); SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, CategoryDTO::getCategoryName, "-- Select Category --", @@ -134,16 +144,11 @@ public class ProductDetailFragment extends Fragment { if (a != null && a.containsKey("prodId")) { isEditing = true; prodId = a.getLong("prodId"); - preselectedCategoryId = a.getLong("categoryId", -1); - hasImage = true; - binding.tvProductMode.setText("Edit Product"); binding.tvProductId.setText("ID: " + prodId); binding.tvProductId.setVisibility(View.VISIBLE); - binding.etProductName.setText(a.getString("prodName")); - binding.etProductDesc.setText(a.getString("prodDesc")); - binding.etProductPrice.setText(a.getString("prodPrice")); binding.btnDeleteProduct.setVisibility(View.VISIBLE); + loadProductData(); loadProductImage(); } else { binding.tvProductMode.setText("Add Product"); @@ -153,6 +158,31 @@ public class ProductDetailFragment extends Fragment { } } + /** + * Loads the product data from the backend. + */ + private void loadProductData() { + viewModel.getProductById(prodId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + ProductDTO p = resource.data; + binding.etProductName.setText(p.getProdName()); + binding.etProductDesc.setText(p.getProdDesc()); + binding.etProductPrice.setText(p.getProdPrice() != null ? p.getProdPrice().toString() : ""); + preselectedCategoryId = p.getCategoryId() != null ? p.getCategoryId() : -1; + + // Refresh spinner selection once data is loaded + if (!categoryList.isEmpty()) { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerProductCategory, categoryList, + CategoryDTO::getCategoryName, "-- Select Category --", + preselectedCategoryId, CategoryDTO::getCategoryId); + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load product: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Loads the product image from the backend. */ @@ -179,8 +209,8 @@ public class ProductDetailFragment extends Fragment { private void performPendingImageActions(String successMsg) { if (isImageRemoved) { viewModel.deleteProductImage(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); @@ -211,8 +241,8 @@ public class ProductDetailFragment extends Fragment { MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); viewModel.uploadProductImage(prodId, body).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); @@ -246,8 +276,8 @@ public class ProductDetailFragment extends Fragment { if (isEditing) { viewModel.updateProduct(prodId, dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { performPendingImageActions("Updated"); } else { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); @@ -256,8 +286,8 @@ public class ProductDetailFragment extends Fragment { }); } else { viewModel.createProduct(dto).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != com.example.petstoremobile.utils.Resource.Status.LOADING) { - if (resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS && resource.data != null) { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { prodId = resource.data.getProdId(); performPendingImageActions("Saved"); } else { @@ -274,9 +304,9 @@ public class ProductDetailFragment extends Fragment { private void confirmDelete() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Product", () -> viewModel.deleteProduct(prodId).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.SUCCESS) { + if (resource != null && resource.status == Resource.Status.SUCCESS) { navigateBack(); - } else if (resource != null && resource.status == com.example.petstoremobile.utils.Resource.Status.ERROR) { + } else if (resource != null && resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } })); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 4fd23fa0..495c0f6d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.*; import android.widget.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -56,13 +57,18 @@ public class ProductSupplierDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentProductSupplierDetailBinding.inflate(inflater, container, false); - loadData(); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + loadSpinnersData(); handleArguments(); binding.btnPSBack.setOnClickListener(v -> navigateBack()); binding.btnSavePS.setOnClickListener(v -> save()); binding.btnDeletePS.setOnClickListener(v -> confirmDelete()); - return binding.getRoot(); } @Override @@ -74,7 +80,7 @@ public class ProductSupplierDetailFragment extends Fragment { /** * Fetches products and suppliers to populate the spinners. */ - private void loadData() { + private void loadSpinnersData() { loadProducts(); loadSuppliers(); } @@ -86,13 +92,17 @@ public class ProductSupplierDetailFragment extends Fragment { productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, - ProductDTO::getProdName, "-- Select Product --", - preselectedProductId, ProductDTO::getProdId); + refreshProductSpinner(); } }); } + private void refreshProductSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); + } + /** * Loads the list of suppliers from the API. */ @@ -100,33 +110,39 @@ public class ProductSupplierDetailFragment extends Fragment { supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { supplierList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, - SupplierDTO::getSupCompany, "-- Select Supplier --", - preselectedSupplierId, SupplierDTO::getSupId); + refreshSupplierSpinner(); } }); } + private void refreshSupplierSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerPSSupplier, supplierList, + SupplierDTO::getSupCompany, "-- Select Supplier --", + preselectedSupplierId, SupplierDTO::getSupId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ private void handleArguments() { Bundle a = getArguments(); - if (a != null && a.containsKey("productId")) { + if (a != null && a.containsKey("productId") && a.containsKey("supplierId")) { isEditing = true; - editProductId = a.getLong("productId"); - editSupplierId = a.getLong("supplierId"); - preselectedProductId = editProductId; + editProductId = a.getLong("productId"); + editSupplierId = a.getLong("supplierId"); + preselectedProductId = editProductId; preselectedSupplierId = editSupplierId; - binding.etPSCost.setText(a.getString("cost")); + binding.tvPSMode.setText("Edit Product Supplier"); binding.btnDeletePS.setVisibility(View.VISIBLE); + } else { binding.tvPSMode.setText("Add Product Supplier"); binding.btnDeletePS.setVisibility(View.GONE); } } + /** * Validates input and saves the product-supplier to the backend. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java index 5d9cf5a9..4a30e4cd 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PurchaseOrderDetailFragment.java @@ -3,11 +3,18 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.graphics.Color; import android.os.Bundle; import android.view.*; +import android.widget.Toast; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import com.example.petstoremobile.databinding.FragmentPurchaseOrderDetailBinding; +import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; import dagger.hilt.android.AndroidEntryPoint; @@ -18,40 +25,72 @@ import dagger.hilt.android.AndroidEntryPoint; public class PurchaseOrderDetailFragment extends Fragment { private FragmentPurchaseOrderDetailBinding binding; + private PurchaseOrderViewModel viewModel; + private long purchaseOrderId; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + } /** - * Inflates the layout, initializes views, and populates order data from arguments. + * Inflates the layout. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentPurchaseOrderDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } - Bundle a = getArguments(); - if (a != null) { - binding.tvPODetailId.setText("PO #" + a.getLong("purchaseOrderId")); - binding.tvPODetailSupplier.setText(a.getString("supplierName")); - binding.tvPODetailDate.setText(a.getString("orderDate")); + /** + * Initializes views and populates order data from backend after the view is created. + */ + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - String status = a.getString("status", ""); - binding.tvPODetailStatus.setText(status); - switch (status) { - case "Completed": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; - case "Pending": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; - case "Cancelled": - binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; - default: - binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; - } - } + handleArguments(); binding.btnPOBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); + } - return binding.getRoot(); + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.containsKey("purchaseOrderId")) { + purchaseOrderId = a.getLong("purchaseOrderId"); + loadPurchaseOrderData(); + } + } + + private void loadPurchaseOrderData() { + viewModel.getPurchaseOrderById(purchaseOrderId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PurchaseOrderDTO po = resource.data; + binding.tvPODetailId.setText("PO #" + po.getPurchaseOrderId()); + binding.tvPODetailSupplier.setText(po.getSupplierName()); + binding.tvPODetailDate.setText(po.getOrderDate()); + + String status = po.getStatus() != null ? po.getStatus() : ""; + binding.tvPODetailStatus.setText(status); + switch (status) { + case "Completed": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50")); break; + case "Pending": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800")); break; + case "Cancelled": + binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336")); break; + default: + binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E")); break; + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java index f4ce5f91..ce532696 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java @@ -2,6 +2,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.navigation.fragment.NavHostFragment; @@ -38,15 +39,18 @@ public class RefundDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentRefundDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setupSpinner(); handleArguments(); binding.btnRefundBack.setOnClickListener(v -> goBack()); binding.btnLoadSale.setOnClickListener(v -> loadSaleDetails()); binding.btnProcessRefund.setOnClickListener(v -> processRefund()); - - return binding.getRoot(); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java index 7f9c053b..49c51141 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class ServiceDetailFragment extends Fragment { private FragmentServiceDetailBinding binding; - private int serviceId; + private long serviceId; private boolean isEditing = false; private ServiceViewModel viewModel; @@ -45,6 +46,12 @@ public class ServiceDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentServiceDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); //get controls from layout and display the view depending on the mode handleArguments(); @@ -53,8 +60,6 @@ public class ServiceDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveService.setOnClickListener(v -> saveService()); binding.btnDeleteService.setOnClickListener(v -> deleteService()); - - return binding.getRoot(); } @Override @@ -89,10 +94,10 @@ public class ServiceDetailFragment extends Fragment { //check if the service is being edited or added if (isEditing) { // Update existing service - serviceDTO.setServiceId((long) serviceId); - viewModel.updateService((long) serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { + serviceDTO.setServiceId(serviceId); + viewModel.updateService(serviceId, serviceDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId); + ActivityLogger.logChange(requireContext(), "Service", "UPDATED", (int) serviceId); Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -117,9 +122,9 @@ public class ServiceDetailFragment extends Fragment { */ private void deleteService() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Service", () -> - viewModel.deleteService((long) serviceId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteService(serviceId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId); + ActivityLogger.logChange(requireContext(), "Service", "DELETED", (int) serviceId); Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -143,14 +148,11 @@ public class ServiceDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("serviceId")) { // Get service data from arguments and populate fields isEditing = true; - serviceId = getArguments().getInt("serviceId"); + serviceId = getArguments().getLong("serviceId"); binding.tvMode.setText("Edit Service"); binding.tvServiceId.setText("ID: " + serviceId); - binding.etServiceName.setText(getArguments().getString("serviceName")); - binding.etServiceDesc.setText(getArguments().getString("serviceDesc")); - binding.etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration"))); - binding.etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice"))); binding.btnDeleteService.setVisibility(View.VISIBLE); + loadServiceData(); } else { // Service is being added // Set default values for add a new service @@ -161,4 +163,22 @@ public class ServiceDetailFragment extends Fragment { binding.btnSaveService.setText("Add"); } } + + /** + * Fetches specific service details from the backend using the ID. + */ + private void loadServiceData() { + viewModel.getServiceById(serviceId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + ServiceDTO s = resource.data; + binding.etServiceName.setText(s.getServiceName()); + binding.etServiceDesc.setText(s.getServiceDesc()); + binding.etServiceDuration.setText(String.valueOf(s.getServiceDuration())); + binding.etServicePrice.setText(String.valueOf(s.getServicePrice())); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load service: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java index 5d52606d..4935cb8b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; @@ -30,7 +31,7 @@ import dagger.hilt.android.AndroidEntryPoint; public class SupplierDetailFragment extends Fragment { private FragmentSupplierDetailBinding binding; - private int supId; + private long supId; private boolean isEditing = false; private SupplierViewModel viewModel; @@ -45,6 +46,12 @@ public class SupplierDetailFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentSupplierDetailBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); // Add phone number formatting (CA) and limit length to 14 characters UIUtils.formatPhoneInput(binding.etSupPhone); @@ -55,8 +62,6 @@ public class SupplierDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSaveSupplier.setOnClickListener(v -> saveSupplier()); binding.btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); - - return binding.getRoot(); } @Override @@ -94,10 +99,10 @@ public class SupplierDetailFragment extends Fragment { //check if the supplier is being edited or added if (isEditing) { // Update existing supplier - supplierDTO.setSupId((long) supId); - viewModel.updateSupplier((long) supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { + supplierDTO.setSupId(supId); + viewModel.updateSupplier(supId, supplierDTO).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId); + ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", (int) supId); Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -123,9 +128,9 @@ public class SupplierDetailFragment extends Fragment { */ private void deleteSupplier() { DialogUtils.showDeleteConfirmDialog(requireContext(), "Supplier", () -> - viewModel.deleteSupplier((long) supId).observe(getViewLifecycleOwner(), resource -> { + viewModel.deleteSupplier(supId).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS) { - ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId); + ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", (int) supId); Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); navigateBack(); } else if (resource.status == Resource.Status.ERROR) { @@ -149,15 +154,12 @@ public class SupplierDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("supId")) { // Get supplier data from arguments and populate fields isEditing = true; - supId = getArguments().getInt("supId"); + supId = getArguments().getLong("supId"); binding.tvMode.setText("Edit Supplier"); binding.tvSupId.setText("ID: " + supId); - binding.etSupCompany.setText(getArguments().getString("supCompany")); - binding.etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); - binding.etSupContactLastName.setText(getArguments().getString("supContactLastName")); - binding.etSupEmail.setText(getArguments().getString("supEmail")); - binding.etSupPhone.setText(getArguments().getString("supPhone")); + binding.tvSupId.setVisibility(View.VISIBLE); binding.btnDeleteSupplier.setVisibility(View.VISIBLE); + loadSupplierData(); } else { // Supplier is being added // Set default values for add a new supplier @@ -168,4 +170,23 @@ public class SupplierDetailFragment extends Fragment { binding.btnSaveSupplier.setText("Add"); } } + + /** + * Fetches specific supplier details from the backend using the ID. + */ + private void loadSupplierData() { + viewModel.getSupplierById(supId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + SupplierDTO s = resource.data; + binding.etSupCompany.setText(s.getSupCompany()); + binding.etSupContactFirstName.setText(s.getSupContactFirstName()); + binding.etSupContactLastName.setText(s.getSupContactLastName()); + binding.etSupEmail.setText(s.getSupEmail()); + binding.etSupPhone.setText(s.getSupPhone()); + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load supplier: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 0ddc0511..69391412 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -3,24 +3,27 @@ package com.example.petstoremobile.fragments.listfragments.listprofilefragments; import android.net.Uri; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; +import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.databinding.FragmentPetProfileBinding; +import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.FileUtils; import com.example.petstoremobile.utils.GlideUtils; import com.example.petstoremobile.utils.ImagePickerHelper; -import com.example.petstoremobile.utils.RetrofitUtils; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.viewmodels.PetViewModel; import java.io.File; import java.util.Locale; @@ -36,16 +39,14 @@ import okhttp3.RequestBody; @AndroidEntryPoint public class PetProfileFragment extends Fragment { - private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; - private Button btnBack, btnEditPet, btnChangePhoto; - private ImageView imgPet; - private int petId; + private FragmentPetProfileBinding binding; + private long petId; private boolean hasImage = false; - @Inject PetApi petApi; @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; + private PetViewModel viewModel; private ImagePickerHelper imagePickerHelper; @@ -55,6 +56,7 @@ public class PetProfileFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PetViewModel.class); imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { @Override @@ -70,65 +72,77 @@ public class PetProfileFragment extends Fragment { } /** - * Inflates the layout, initializes views, and sets up click listeners. + * Inflates the layout using view binding, initializes views, and sets up click listeners. */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_pet_profile, container, false); - - // Initialize views - tvPetName = view.findViewById(R.id.tvPetName); - tvPetSpecies = view.findViewById(R.id.tvPetSpecies); - tvPetBreed = view.findViewById(R.id.tvPetBreed); - tvPetAge = view.findViewById(R.id.tvPetAge); - tvPetPrice = view.findViewById(R.id.tvPetPrice); - btnBack = view.findViewById(R.id.btnBack); - btnEditPet = view.findViewById(R.id.btnEditPet); - btnChangePhoto = view.findViewById(R.id.btnChangePhoto); - imgPet = view.findViewById(R.id.imgPet); - + binding = FragmentPetProfileBinding.inflate(inflater, container, false); // Set pet details to display if (getArguments() != null) { - petId = getArguments().getInt("petId"); - tvPetName.setText(getArguments().getString("petName")); - tvPetSpecies.setText(getArguments().getString("petSpecies")); - tvPetBreed.setText(getArguments().getString("petBreed")); - tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge"))); - tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice"))); - - // Load pet image from backend - loadPetImage(petId); + petId = getArguments().getLong("petId"); + loadPetData(); + loadPetImage((int) petId); } //set button click listeners - btnBack.setOnClickListener(v -> { + binding.btnBack.setOnClickListener(v -> { NavHostFragment.findNavController(this).popBackStack(); }); //Make the edit button go to the pet detail view - btnEditPet.setOnClickListener(v -> { - if (getArguments() == null) return; - NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, getArguments()); + binding.btnEditPet.setOnClickListener(v -> { + Bundle args = new Bundle(); + args.putLong("petId", petId); + NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail, args); }); //Make change photo button ask user to select a new photo - btnChangePhoto.setOnClickListener(v -> { + binding.btnChangePhoto.setOnClickListener(v -> { imagePickerHelper.showImagePickerDialog("Change Pet Photo", hasImage); }); - return view; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } /** - * Fetches and displays the pet's image from the server. + * Fetches current pet data from the backend and updates the UI. + */ + private void loadPetData() { + viewModel.getPetById(petId).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + PetDTO pet = resource.data; + binding.tvPetName.setText(pet.getPetName()); + binding.tvPetSpecies.setText(pet.getPetSpecies()); + binding.tvPetBreed.setText(pet.getPetBreed()); + binding.tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", pet.getPetAge())); + try { + binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", Double.parseDouble(pet.getPetPrice()))); + } catch (Exception e) { + binding.tvPetPrice.setText("$0.00"); + } + } else if (resource.status == Resource.Status.ERROR) { + Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * Fetches and displays the pet\'s image from the server. */ private void loadPetImage(int petId) { String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId); String token = tokenManager.getToken(); - GlideUtils.loadImageWithToken(requireContext(), imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { + GlideUtils.loadImageWithToken(requireContext(), binding.imgPet, imageUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() { @Override public void onResourceReady() { hasImage = true; @@ -142,7 +156,7 @@ public class PetProfileFragment extends Fragment { } /** - * Uploads a selected or captured image a pet photo through the API. + * Uploads a selected or captured image a pet photo through the ViewModel. */ private void uploadPetImage(Uri uri) { try { @@ -153,30 +167,36 @@ public class PetProfileFragment extends Fragment { RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); - // Call the backend to upload the image - petApi.uploadPetImage((long) petId, body).enqueue(RetrofitUtils.createCallback( - requireContext(), - "UPLOAD_PET_IMAGE", - "Pet photo updated successfully", - result -> loadPetImage(petId) - )); + // Use ViewModel to upload image + viewModel.uploadPetImage(petId, body).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show(); + loadPetImage((int) petId); + } else { + Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + } + }); } catch (Exception e) { Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage()); } } /** - * Sends a request to the API to remove the current pet photo. + * Sends a request to the ViewModel to remove the current pet photo. */ private void deletePetImage() { - petApi.deletePetImage((long) petId).enqueue(RetrofitUtils.createCallback( - requireContext(), - "DELETE_PET_IMAGE", - "Pet photo removed", - result -> { + viewModel.deletePetImage(petId).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); hasImage = false; - imgPet.setImageResource(R.drawable.placeholder); + binding.imgPet.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); } - )); + } + }); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java index f5a5d2c5..0e73b706 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AdoptionRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.AdoptionApi; import com.example.petstoremobile.dtos.AdoptionDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class AdoptionRepository { - private static final String TAG = "AdoptionRepository"; +public class AdoptionRepository extends BaseRepository { private final AdoptionApi adoptionApi; @Inject public AdoptionRepository(AdoptionApi adoptionApi) { + super("AdoptionRepository"); this.adoptionApi = adoptionApi; } @@ -26,44 +24,34 @@ public class AdoptionRepository { * Retrieves a paginated list of all adoptions from the API. */ public LiveData>> getAllAdoptions(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.getAllAdoptions(page, size), data, TAG); - return data; + return executeCall(adoptionApi.getAllAdoptions(page, size)); } /** * Retrieves a specific adoption record by its ID from the API. */ public LiveData> getAdoptionById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.getAdoptionById(id), data, TAG); - return data; + return executeCall(adoptionApi.getAdoptionById(id)); } /** * Sends a request to the API to create a new adoption record. */ public LiveData> createAdoption(AdoptionDTO adoption) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.createAdoption(adoption), data, TAG); - return data; + return executeCall(adoptionApi.createAdoption(adoption)); } /** * Sends a request to the API to update an existing adoption record by ID. */ public LiveData> updateAdoption(Long id, AdoptionDTO adoption) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.updateAdoption(id, adoption), data, TAG); - return data; + return executeCall(adoptionApi.updateAdoption(id, adoption)); } /** * Sends a request to the API to delete a specific adoption record. */ public LiveData> deleteAdoption(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(adoptionApi.deleteAdoption(id), data, TAG); - return data; + return executeCall(adoptionApi.deleteAdoption(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 61f56842..30e25d0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.AppointmentApi; import com.example.petstoremobile.dtos.AppointmentDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class AppointmentRepository { - private static final String TAG = "AppointmentRepository"; +public class AppointmentRepository extends BaseRepository { private final AppointmentApi appointmentApi; @Inject public AppointmentRepository(AppointmentApi appointmentApi) { + super("AppointmentRepository"); this.appointmentApi = appointmentApi; } @@ -26,44 +24,34 @@ public class AppointmentRepository { * Retrieves a paginated list of all appointments from the API. */ public LiveData>> getAllAppointments(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.getAllAppointments(page, size), data, TAG); - return data; + return executeCall(appointmentApi.getAllAppointments(page, size)); } /** * Retrieves a specific appointment by its ID from the API. */ public LiveData> getAppointmentById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.getAppointmentById(id), data, TAG); - return data; + return executeCall(appointmentApi.getAppointmentById(id)); } /** * Sends a request to the API to create a new appointment record. */ public LiveData> createAppointment(AppointmentDTO appointment) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.createAppointment(appointment), data, TAG); - return data; + return executeCall(appointmentApi.createAppointment(appointment)); } /** * Sends a request to the API to update an existing appointment record by ID. */ public LiveData> updateAppointment(Long id, AppointmentDTO appointment) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.updateAppointment(id, appointment), data, TAG); - return data; + return executeCall(appointmentApi.updateAppointment(id, appointment)); } /** * Sends a request to the API to delete a specific appointment record. */ public LiveData> deleteAppointment(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(appointmentApi.deleteAppointment(id), data, TAG); - return data; + return executeCall(appointmentApi.deleteAppointment(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java index dec9613e..2ec410f9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AuthRepository.java @@ -1,7 +1,5 @@ package com.example.petstoremobile.repositories; -import android.util.Log; - import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -12,7 +10,6 @@ import com.example.petstoremobile.dtos.AuthDTO; import com.example.petstoremobile.dtos.UserDTO; import com.example.petstoremobile.utils.ErrorUtils; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import java.util.Map; @@ -25,13 +22,13 @@ import retrofit2.Callback; import retrofit2.Response; @Singleton -public class AuthRepository { - private static final String TAG = "AuthRepository"; +public class AuthRepository extends BaseRepository { private final AuthApi authApi; private final TokenManager tokenManager; @Inject public AuthRepository(AuthApi authApi, TokenManager tokenManager) { + super("AuthRepository"); this.authApi = authApi; this.tokenManager = tokenManager; } @@ -62,7 +59,7 @@ public class AuthRepository { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - data.setValue(Resource.error("Network error: " + t.getMessage(), null)); + data.setValue(Resource.error(ErrorUtils.getFailureMessage(t), null)); } }); @@ -73,36 +70,28 @@ public class AuthRepository { * Retrieves the current user's profile information from the API. */ public LiveData> getMe() { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.getMe(), data, TAG); - return data; + return executeCall(authApi.getMe()); } /** * Updates the current user's profile details. */ public LiveData> updateMe(Map updates) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.updateMe(updates), data, TAG); - return data; + return executeCall(authApi.updateMe(updates)); } /** * Uploads a multipart image to be used as the current user's avatar. */ public LiveData> uploadAvatar(MultipartBody.Part avatar) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.uploadAvatar(avatar), data, TAG); - return data; + return executeCall(authApi.uploadAvatar(avatar)); } /** * Sends a request to the API to remove the current user's avatar. */ public LiveData> deleteAvatar() { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(authApi.deleteAvatar(), data, TAG); - return data; + return executeCall(authApi.deleteAvatar()); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java new file mode 100644 index 00000000..cf98dfe8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/BaseRepository.java @@ -0,0 +1,29 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.RetrofitUtils; + +import retrofit2.Call; + +/** + * Base class for all repositories to provide common functionality for API calls. + */ +public abstract class BaseRepository { + protected final String TAG; + + protected BaseRepository(String tag) { + this.TAG = tag; + } + + /** + * Executes a Retrofit call and returns a LiveData containing the Resource. + */ + protected LiveData> executeCall(Call call) { + MutableLiveData> data = new MutableLiveData<>(); + RetrofitUtils.enqueue(call, data, TAG); + return data; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java index 74516e2b..8d11511b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CategoryRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.CategoryApi; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class CategoryRepository { - private static final String TAG = "CategoryRepository"; +public class CategoryRepository extends BaseRepository { private final CategoryApi categoryApi; @Inject public CategoryRepository(CategoryApi categoryApi) { + super("CategoryRepository"); this.categoryApi = categoryApi; } @@ -26,8 +24,6 @@ public class CategoryRepository { * Retrieves a paginated list of all product categories from the API. */ public LiveData>> getAllCategories(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(categoryApi.getAllCategories(page, size), data, TAG); - return data; + return executeCall(categoryApi.getAllCategories(page, size)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java new file mode 100644 index 00000000..8301797d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ChatRepository.java @@ -0,0 +1,74 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.ChatApi; +import com.example.petstoremobile.api.CustomerApi; +import com.example.petstoremobile.api.MessageApi; +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SendMessageRequest; +import com.example.petstoremobile.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +/** + * Repository for handling chat-related data operations. + */ +@Singleton +public class ChatRepository extends BaseRepository { + private final ChatApi chatApi; + private final MessageApi messageApi; + private final CustomerApi customerApi; + + @Inject + public ChatRepository(ChatApi chatApi, MessageApi messageApi, CustomerApi customerApi) { + super("ChatRepository"); + this.chatApi = chatApi; + this.messageApi = messageApi; + this.customerApi = customerApi; + } + + /** + * Retrieves all chat conversations for the current user. + */ + public LiveData>> getAllConversations() { + return executeCall(chatApi.getAllConversations()); + } + + /** + * Retrieves the message history for a specific conversation. + */ + public LiveData>> getMessages(Long conversationId) { + return executeCall(messageApi.getMessages(conversationId)); + } + + /** + * Sends a plain text message to a conversation. + */ + public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { + return executeCall(messageApi.sendMessage(conversationId, request)); + } + + /** + * Sends a message with a file attachment to a conversation. + */ + public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { + return executeCall(messageApi.sendMessageWithAttachment(conversationId, content, file)); + } + + /** + * Fetches a paginated list of customers. + */ + public LiveData>> getAllCustomers(int page, int size) { + return executeCall(customerApi.getAllCustomers(page, size)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java index 834f9197..4006ae69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/CustomerRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.CustomerApi; import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class CustomerRepository { - private static final String TAG = "CustomerRepository"; +public class CustomerRepository extends BaseRepository { private final CustomerApi customerApi; @Inject public CustomerRepository(CustomerApi customerApi) { + super("CustomerRepository"); this.customerApi = customerApi; } @@ -26,17 +24,13 @@ public class CustomerRepository { * Retrieves a paginated list of all customers from the API. */ public LiveData>> getAllCustomers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(customerApi.getAllCustomers(page, size), data, TAG); - return data; + return executeCall(customerApi.getAllCustomers(page, size)); } /** * Retrieves a specific customer by their ID. */ public LiveData> getCustomerById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(customerApi.getCustomerById(id), data, TAG); - return data; + return executeCall(customerApi.getCustomerById(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 4fd2e66d..5513d0e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -1,7 +1,6 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.dtos.BulkDeleteRequest; @@ -9,20 +8,17 @@ import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; - -import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class InventoryRepository { - private static final String TAG = "InventoryRepository"; +public class InventoryRepository extends BaseRepository { private final InventoryApi inventoryApi; @Inject public InventoryRepository(InventoryApi inventoryApi) { + super("InventoryRepository"); this.inventoryApi = inventoryApi; } @@ -30,47 +26,35 @@ public class InventoryRepository { * Retrieves a paginated list of inventory items from the API with optional search and sort. */ public LiveData>> getAllInventory(String query, int page, int size, String sort) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.getAllInventory(query, page, size, sort), data, TAG); - return data; + return executeCall(inventoryApi.getAllInventory(query, page, size, sort)); } /** * Retrieves a specific inventory item by its ID from the API. */ public LiveData> getInventoryById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.getInventoryById(id), data, TAG); - return data; + return executeCall(inventoryApi.getInventoryById(id)); } /** * Sends a request to the API to create a new inventory record. */ public LiveData> createInventory(InventoryRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.createInventory(request), data, TAG); - return data; + return executeCall(inventoryApi.createInventory(request)); } public LiveData> updateInventory(Long id, InventoryRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.updateInventory(id, request), data, TAG); - return data; + return executeCall(inventoryApi.updateInventory(id, request)); } /** * Sends a request to the API to delete a specific inventory record. */ public LiveData> deleteInventory(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.deleteInventory(id), data, TAG); - return data; + return executeCall(inventoryApi.deleteInventory(id)); } public LiveData> bulkDeleteInventory(BulkDeleteRequest request) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(inventoryApi.bulkDeleteInventory(request), data, TAG); - return data; + return executeCall(inventoryApi.bulkDeleteInventory(request)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index f46bb9e3..64f23b78 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -1,13 +1,11 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -15,12 +13,12 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; @Singleton -public class PetRepository { - private static final String TAG = "PetRepository"; +public class PetRepository extends BaseRepository { private final PetApi petApi; @Inject public PetRepository(PetApi petApi) { + super("PetRepository"); this.petApi = petApi; } @@ -28,62 +26,48 @@ public class PetRepository { * Retrieves a paginated list of all pets from the API. */ public LiveData>> getAllPets(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.getAllPets(page, size), data, TAG); - return data; + return executeCall(petApi.getAllPets(page, size)); } /** * Retrieves a specific pet by its ID from the API. */ public LiveData> getPetById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.getPetById(id), data, TAG); - return data; + return executeCall(petApi.getPetById(id)); } /** * Sends a request to the API to create a new pet record. */ public LiveData> createPet(PetDTO pet) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.createPet(pet), data, TAG); - return data; + return executeCall(petApi.createPet(pet)); } /** * Sends a request to the API to update an existing pet record. */ public LiveData> updatePet(Long id, PetDTO pet) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.updatePet(id, pet), data, TAG); - return data; + return executeCall(petApi.updatePet(id, pet)); } /** * Sends a request to the API to delete a specific pet record. */ public LiveData> deletePet(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.deletePet(id), data, TAG); - return data; + return executeCall(petApi.deletePet(id)); } /** * Uploads an image file for a specific pet via the API. */ public LiveData> uploadPetImage(Long id, MultipartBody.Part image) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.uploadPetImage(id, image), data, TAG); - return data; + return executeCall(petApi.uploadPetImage(id, image)); } /** * Sends a request to the API to delete the image of a specific pet. */ public LiveData> deletePetImage(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(petApi.deletePetImage(id), data, TAG); - return data; + return executeCall(petApi.deletePetImage(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 0baf5c4a..5ed95c8a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -1,13 +1,11 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ProductApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -15,12 +13,12 @@ import javax.inject.Singleton; import okhttp3.MultipartBody; @Singleton -public class ProductRepository { - private static final String TAG = "ProductRepository"; +public class ProductRepository extends BaseRepository { private final ProductApi productApi; @Inject public ProductRepository(ProductApi productApi) { + super("ProductRepository"); this.productApi = productApi; } @@ -28,62 +26,48 @@ public class ProductRepository { * Retrieves a paginated list of products from the API, filtered by an optional query. */ public LiveData>> getAllProducts(String query, int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.getAllProducts(query, page, size), data, TAG); - return data; + return executeCall(productApi.getAllProducts(query, page, size)); } /** * Retrieves a specific product by its ID from the API. */ public LiveData> getProductById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.getProductById(id), data, TAG); - return data; + return executeCall(productApi.getProductById(id)); } /** * Sends a request to the API to create a new product. */ public LiveData> createProduct(ProductDTO product) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.createProduct(product), data, TAG); - return data; + return executeCall(productApi.createProduct(product)); } /** * Sends a request to the API to update an existing product by ID. */ public LiveData> updateProduct(Long id, ProductDTO product) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.updateProduct(id, product), data, TAG); - return data; + return executeCall(productApi.updateProduct(id, product)); } /** * Sends a request to the API to delete a specific product. */ public LiveData> deleteProduct(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.deleteProduct(id), data, TAG); - return data; + return executeCall(productApi.deleteProduct(id)); } /** * Uploads an image file for a specific product via the API. */ public LiveData> uploadProductImage(Long id, MultipartBody.Part image) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.uploadProductImage(id, image), data, TAG); - return data; + return executeCall(productApi.uploadProductImage(id, image)); } /** * Sends a request to the API to delete the image of a specific product. */ public LiveData> deleteProductImage(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(productApi.deleteProductImage(id), data, TAG); - return data; + return executeCall(productApi.deleteProductImage(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index e92472c0..72182918 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ProductSupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductSupplierDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class ProductSupplierRepository { - private static final String TAG = "ProductSupplierRepository"; +public class ProductSupplierRepository extends BaseRepository { private final ProductSupplierApi api; @Inject public ProductSupplierRepository(ProductSupplierApi api) { + super("ProductSupplierRepository"); this.api = api; } @@ -26,35 +24,34 @@ public class ProductSupplierRepository { * Retrieves a paginated list of all product-supplier relationships from the API. */ public LiveData>> getAllProductSuppliers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getAllProductSuppliers(page, size), data, TAG); - return data; + return executeCall(api.getAllProductSuppliers(page, size)); + } + + /** + * Retrieves a single product-supplier relationship by product and supplier IDs. + */ + public LiveData> getProductSupplierById(Long productId, Long supplierId) { + return executeCall(api.getProductSupplierById(productId, supplierId)); } /** * Sends a request to the API to create a new product-supplier relationship. */ public LiveData> createProductSupplier(ProductSupplierDTO dto) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.createProductSupplier(dto), data, TAG); - return data; + return executeCall(api.createProductSupplier(dto)); } /** * Sends a request to the API to update an existing product-supplier relationship. */ public LiveData> updateProductSupplier(Long productId, Long supplierId, ProductSupplierDTO dto) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.updateProductSupplier(productId, supplierId, dto), data, TAG); - return data; + return executeCall(api.updateProductSupplier(productId, supplierId, dto)); } /** * Sends a request to the API to delete a specific product-supplier relationship. */ public LiveData> deleteProductSupplier(Long productId, Long supplierId) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.deleteProductSupplier(productId, supplierId), data, TAG); - return data; + return executeCall(api.deleteProductSupplier(productId, supplierId)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index f00400e5..bd1b224e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.PurchaseOrderApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class PurchaseOrderRepository { - private static final String TAG = "PurchaseOrderRepo"; +public class PurchaseOrderRepository extends BaseRepository { private final PurchaseOrderApi api; @Inject public PurchaseOrderRepository(PurchaseOrderApi api) { + super("PurchaseOrderRepo"); this.api = api; } @@ -26,17 +24,13 @@ public class PurchaseOrderRepository { * Retrieves a paginated list of all purchase orders from the API. */ public LiveData>> getAllPurchaseOrders(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getAllPurchaseOrders(page, size), data, TAG); - return data; + return executeCall(api.getAllPurchaseOrders(page, size)); } /** * Retrieves a specific purchase order by its ID from the API. */ public LiveData> getPurchaseOrderById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(api.getPurchaseOrderById(id), data, TAG); - return data; + return executeCall(api.getPurchaseOrderById(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index ca6021e6..eac8bb32 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.ServiceApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ServiceDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class ServiceRepository { - private static final String TAG = "ServiceRepository"; +public class ServiceRepository extends BaseRepository { private final ServiceApi serviceApi; @Inject public ServiceRepository(ServiceApi serviceApi) { + super("ServiceRepository"); this.serviceApi = serviceApi; } @@ -26,44 +24,34 @@ public class ServiceRepository { * Retrieves a paginated list of all services from the API. */ public LiveData>> getAllServices(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.getAllServices(page, size), data, TAG); - return data; + return executeCall(serviceApi.getAllServices(page, size)); } /** * Retrieves a specific service by its ID from the API. */ public LiveData> getServiceById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.getServiceById(id), data, TAG); - return data; + return executeCall(serviceApi.getServiceById(id)); } /** * Sends a request to the API to create a new service. */ public LiveData> createService(ServiceDTO service) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.createService(service), data, TAG); - return data; + return executeCall(serviceApi.createService(service)); } /** * Sends a request to the API to update an existing service by ID. */ public LiveData> updateService(Long id, ServiceDTO service) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.updateService(id, service), data, TAG); - return data; + return executeCall(serviceApi.updateService(id, service)); } /** * Sends a request to the API to delete a specific service. */ public LiveData> deleteService(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(serviceApi.deleteService(id), data, TAG); - return data; + return executeCall(serviceApi.deleteService(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java index a71a7ffe..44781a32 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/StoreRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.StoreApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class StoreRepository { - private static final String TAG = "StoreRepository"; +public class StoreRepository extends BaseRepository { private final StoreApi storeApi; @Inject public StoreRepository(StoreApi storeApi) { + super("StoreRepository"); this.storeApi = storeApi; } @@ -26,8 +24,6 @@ public class StoreRepository { * Retrieves a paginated list of all stores from the API. */ public LiveData>> getAllStores(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(storeApi.getAllStores(page, size), data, TAG); - return data; + return executeCall(storeApi.getAllStores(page, size)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index f169f415..ec0489ca 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -1,24 +1,22 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import com.example.petstoremobile.api.SupplierApi; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.utils.Resource; -import com.example.petstoremobile.utils.RetrofitUtils; import javax.inject.Inject; import javax.inject.Singleton; @Singleton -public class SupplierRepository { - private static final String TAG = "SupplierRepository"; +public class SupplierRepository extends BaseRepository { private final SupplierApi supplierApi; @Inject public SupplierRepository(SupplierApi supplierApi) { + super("SupplierRepository"); this.supplierApi = supplierApi; } @@ -26,44 +24,34 @@ public class SupplierRepository { * Retrieves a paginated list of all suppliers from the API. */ public LiveData>> getAllSuppliers(int page, int size) { - MutableLiveData>> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.getAllSuppliers(page, size), data, TAG); - return data; + return executeCall(supplierApi.getAllSuppliers(page, size)); } /** * Retrieves a specific supplier by its ID from the API. */ public LiveData> getSupplierById(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.getSupplierById(id), data, TAG); - return data; + return executeCall(supplierApi.getSupplierById(id)); } /** * Sends a request to the API to create a new supplier record. */ public LiveData> createSupplier(SupplierDTO supplier) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.createSupplier(supplier), data, TAG); - return data; + return executeCall(supplierApi.createSupplier(supplier)); } /** * Sends a request to the API to update an existing supplier record by ID. */ public LiveData> updateSupplier(Long id, SupplierDTO supplier) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.updateSupplier(id, supplier), data, TAG); - return data; + return executeCall(supplierApi.updateSupplier(id, supplier)); } /** * Sends a request to the API to delete a specific supplier record. */ public LiveData> deleteSupplier(Long id) { - MutableLiveData> data = new MutableLiveData<>(); - RetrofitUtils.enqueue(supplierApi.deleteSupplier(id), data, TAG); - return data; + return executeCall(supplierApi.deleteSupplier(id)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java index a1e69f5f..82a10cf7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ErrorUtils.java @@ -5,12 +5,19 @@ import android.util.Log; import android.widget.Toast; import com.example.petstoremobile.dtos.ErrorResponse; import com.google.gson.Gson; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; import retrofit2.Response; /** * Utility class for handling API error responses. */ public class ErrorUtils { + private static final String TAG = "ErrorUtils"; + private static final Gson gson = new Gson(); + /** * Shows an error message to toast based on the response. */ @@ -19,20 +26,46 @@ public class ErrorUtils { } /** - * Extracts the error message from the response body. + * Extracts a user-friendly error message from the response body or status code. */ public static String getErrorMessage(Response response, String defaultMessage) { + if (response == null) return defaultMessage; + try { - if (response != null && response.errorBody() != null) { + if (response.errorBody() != null) { String errorJson = response.errorBody().string(); - ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class); + ErrorResponse errorResponse = gson.fromJson(errorJson, ErrorResponse.class); if (errorResponse != null && errorResponse.getMessage() != null) { return errorResponse.getMessage(); } } } catch (Exception e) { - Log.e("ErrorUtils", "Error parsing error body", e); + Log.e(TAG, "Error parsing error body", e); + } + + // Handle specific status codes if no message was provided by the API + switch (response.code()) { + case 401: return "Unauthorized. Please login again."; + case 403: return "Access denied."; + case 404: return "Resource not found."; + case 500: return "Internal server error. Please try again later."; + case 503: return "Service unavailable. The server might be down."; + default: return defaultMessage + " (Code: " + response.code() + ")"; + } + } + + /** + * Converts a Throwable (from onFailure) into a user-friendly network error message. + */ + public static String getFailureMessage(Throwable t) { + if (t instanceof UnknownHostException || t instanceof ConnectException) { + return "No internet connection. Please check your settings."; + } else if (t instanceof SocketTimeoutException) { + return "The connection timed out. Please try again."; + } else if (t instanceof IOException) { + return "Network error occurred. Please try again."; + } else { + return "An unexpected error occurred: " + t.getLocalizedMessage(); } - return defaultMessage; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java index 3584f845..f86a6307 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/RetrofitUtils.java @@ -25,7 +25,7 @@ public class RetrofitUtils { } /** - * call and updates the provided MutableLiveData with Resource states. + * Enqueues a Retrofit call and updates the provided MutableLiveData with Resource states. */ public static void enqueue(@NonNull Call call, @NonNull MutableLiveData> data, String tag) { data.setValue(Resource.loading(null)); @@ -43,8 +43,8 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - String errorMsg = "Network Error: " + t.getMessage(); - Log.e(tag, errorMsg); + String errorMsg = ErrorUtils.getFailureMessage(t); + Log.e(tag, "Network Error: " + t.getMessage(), t); data.setValue(Resource.error(errorMsg, null)); } }); @@ -52,6 +52,7 @@ public class RetrofitUtils { /** * Creates a callback for Retrofit calls that handles errors and logging. + * @deprecated Use {@link #enqueue(Call, MutableLiveData, String)} for LiveData-based architecture. */ @Deprecated public static Callback createCallback(Context context, String tag, String successMsg, SuccessCallback successCallback) { @@ -73,8 +74,9 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(tag, "Network Error: " + t.getMessage()); - Toast.makeText(context, "Network error. Please try again.", Toast.LENGTH_SHORT).show(); + String errorMsg = ErrorUtils.getFailureMessage(t); + Log.e(tag, "Network Error: " + t.getMessage(), t); + Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show(); } }; } @@ -96,7 +98,7 @@ public class RetrofitUtils { @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { - Log.e(tag, "Network Error: " + t.getMessage()); + Log.e(tag, "Network Error: " + t.getMessage(), t); } }; } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java new file mode 100644 index 00000000..51435f82 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ChatViewModel.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SendMessageRequest; +import com.example.petstoremobile.repositories.ChatRepository; +import com.example.petstoremobile.utils.Resource; + +import java.util.List; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +/** + * ViewModel for managing chat-related UI state and data operations. + */ +@HiltViewModel +public class ChatViewModel extends ViewModel { + private final ChatRepository repository; + + @Inject + public ChatViewModel(ChatRepository repository) { + this.repository = repository; + } + + /** + * Retrieves all chat conversations for the current user. + */ + public LiveData>> getAllConversations() { + return repository.getAllConversations(); + } + + /** + * Retrieves the message history for a specific conversation. + */ + public LiveData>> getMessages(Long conversationId) { + return repository.getMessages(conversationId); + } + + /** + * Sends a plain text message to a conversation. + */ + public LiveData> sendMessage(Long conversationId, SendMessageRequest request) { + return repository.sendMessage(conversationId, request); + } + + /** + * Sends a message with a file attachment to a conversation. + */ + public LiveData> sendMessageWithAttachment(Long conversationId, RequestBody content, MultipartBody.Part file) { + return repository.sendMessageWithAttachment(conversationId, content, file); + } + + /** + * Fetches a paginated list of customers. + */ + public LiveData>> getAllCustomers(int page, int size) { + return repository.getAllCustomers(page, size); + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index c7a01abf..f1b9518b 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -5,11 +5,16 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" appcompat = "1.7.1" material = "1.13.0" -activity = "1.12.4" +activity = "1.13.0" constraintlayout = "2.2.1" swiperefreshlayout = "1.2.0" hilt = "2.51.1" navigation = "2.8.8" +retrofit = "2.11.0" +okhttp = "4.12.0" +glide = "4.16.0" +viewpager2 = "1.1.0" +camera = "1.4.1" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -25,6 +30,23 @@ hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.r navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigation" } navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigation" } +# Networking +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + +# UI Components +viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" } +glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } +glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide" } + +# CameraX +camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" } +camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camera" } +camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" } +camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7e694390..5ef50837 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,7 @@ #Sun Mar 01 14:36:37 MST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From b5b829013106737e38f87868239b55c0d493f3af Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 13:35:01 -0600 Subject: [PATCH 073/137] Fix Flyway migration --- .gitignore | 3 ++ .../config/FlywayContextInitializer.java | 7 ++-- .../V18__past_appointments_missed.sql | 39 ++++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 733370d6..7a998c43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *.zip .local/ +commit-patches/ +temp_photos/ +uploads/ diff --git a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java index 000ebe86..746d4682 100644 --- a/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/FlywayContextInitializer.java @@ -35,13 +35,14 @@ public class FlywayContextInitializer implements ApplicationContextInitializer a1.appointmentId + SELECT 1 FROM ( + SELECT employeeId, appointmentDate, appointmentTime, appointmentId + FROM appointment + ) snap + WHERE snap.employeeId = es.employeeId + AND snap.appointmentDate = a1.appointmentDate + AND snap.appointmentTime = a1.appointmentTime + AND snap.appointmentId <> a1.appointmentId ) ORDER BY es.employeeId ASC LIMIT 1 -) -WHERE EXISTS ( - SELECT 1 FROM appointment a3 - WHERE a3.employeeId = a1.employeeId - AND a3.appointmentDate = a1.appointmentDate - AND a3.appointmentTime = a1.appointmentTime - AND a3.appointmentId < a1.appointmentId -) AND LOWER(a1.appointmentStatus) NOT IN ('cancelled', 'missed'); +); From cd5dd32c731cf0b2b12e3e975da93176c6d2e574 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 13:26:55 -0600 Subject: [PATCH 074/137] Update Postman collection --- .../petstoremobile/dtos/AdoptionDTO.java | 1 - .../petstoremobile/dtos/AppointmentDTO.java | 8 +- backend/petshop-api.postman_collection.json | 132 +++++++++++++++++- .../backend/service/AppointmentService.java | 4 - .../migration/V16__activate_all_employees.sql | 1 - .../V17__normalize_appointment_pets.sql | 6 - .../V18__past_appointments_missed.sql | 6 - .../backend/service/AdoptionServiceTest.java | 2 +- .../petshopdesktop/DTOs/AppointmentDTO.java | 2 - .../controllers/AdoptionController.java | 8 +- .../controllers/AppointmentController.java | 8 +- .../AdoptionDialogController.java | 4 - .../AppointmentDialogController.java | 32 +---- 13 files changed, 135 insertions(+), 79 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java index 6866f6b0..daf7d768 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AdoptionDTO.java @@ -16,7 +16,6 @@ public class AdoptionDTO { private String createdAt; private String updatedAt; - // Constructor for create/update requests public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) { this(petId, customerId, null, adoptionDate, adoptionStatus); } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java index 05f9ea21..01f8ef5d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AppointmentDTO.java @@ -4,7 +4,7 @@ import java.math.BigDecimal; import java.util.List; public class AppointmentDTO { - // Response fields (from server) + private Long appointmentId; private Long customerId; private String customerName; @@ -22,8 +22,6 @@ public class AppointmentDTO { private String createdAt; private String updatedAt; - // Constructor for CREATE/UPDATE request body - // Matches AppointmentRequest exactly public AppointmentDTO(Long customerId, Long storeId, Long serviceId, String appointmentDate, String appointmentTime, String appointmentStatus, List petIds) { @@ -43,7 +41,6 @@ public class AppointmentDTO { this.petIds = petIds; } - // Getters public Long getAppointmentId() { return appointmentId; } @@ -108,7 +105,6 @@ public class AppointmentDTO { return updatedAt; } - // Convenience getters for adapter/list display public String getPetName() { return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : ""; } @@ -121,7 +117,6 @@ public class AppointmentDTO { return getPetID(); } - // Keep old name so adapter doesn't break public String getServiceType() { return serviceName; } @@ -130,7 +125,6 @@ public class AppointmentDTO { return serviceId; } - // Status alias public String getStatus() { return appointmentStatus; } diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 7db7d61f..73fed551 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -2069,6 +2069,37 @@ { "name": "Appointments", "item": [ + { + "name": "Get Appointment Customers Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/appointment-customers", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Check Appointment Availability", "request": { @@ -2180,7 +2211,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2222,7 +2253,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}", + "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2315,6 +2346,37 @@ { "name": "Adoptions", "item": [ + { + "name": "Get Adoption Pets Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/adoption-pets", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "List Adoptions", "request": { @@ -2395,7 +2457,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\"\n}", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -2437,7 +2499,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\"\n}", + "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}", "options": { "raw": { "language": "json" @@ -3719,6 +3781,68 @@ } ] }, + { + "name": "Get Store Employees Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/stores/{{storeId}}/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Get All Employees Dropdown", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "List Stores", "request": { diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index b4e270fa..155b7524 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -218,7 +218,6 @@ public class AppointmentService { List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); - // Group by employee for faster lookup in the loop java.util.Map> appointmentsByEmployee = allAppointments.stream() .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); @@ -350,7 +349,6 @@ public class AppointmentService { .isPresent(); } - //------------------------------------ private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List existingAppointments = appointmentRepository .findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date); @@ -359,8 +357,6 @@ public class AppointmentService { } } - //------------------------------------------------ - private boolean isSlotAvailable(List existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) { LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration()); for (Appointment existingAppointment : existingAppointments) { diff --git a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql index cbabc11d..314c86c8 100644 --- a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql +++ b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql @@ -1,4 +1,3 @@ --- Activate all employees in the users table so they appear in dropdowns UPDATE users u SET u.active = TRUE WHERE u.role IN ('STAFF', 'ADMIN') diff --git a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql index 00d34751..90c2a407 100644 --- a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql +++ b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql @@ -1,7 +1,3 @@ --- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet - --- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet --- Note: pet species and breed might be null in pet table, but we copy them over if present INSERT INTO customer_pet (customer_id, pet_name, species, breed) SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed FROM appointmentPet ap @@ -12,7 +8,6 @@ WHERE NOT EXISTS ( WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName ); --- Step 2: Link the appointment to the customer_pet INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) SELECT ap.appointmentId, cp.customer_pet_id FROM appointmentPet ap @@ -24,5 +19,4 @@ WHERE NOT EXISTS ( WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id ); --- Step 3: Remove the old legacy relationships so it strictly uses the new ones DELETE FROM appointmentPet; diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql index 19ba0fa0..093c6ce3 100644 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql @@ -1,7 +1,3 @@ --- V18: Normalize past appointments and resolve initial employee double-bookings - --- Part 1: Normalize past appointments. --- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'. UPDATE appointment SET appointmentStatus = 'Missed' WHERE LOWER(appointmentStatus) = 'booked' @@ -10,8 +6,6 @@ WHERE LOWER(appointmentStatus) = 'booked' OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) ); --- Part 2: Resolve potential double-bookings caused by V15's simple backfill. --- MySQL Error 1093 workaround: wrap same-table subqueries in derived tables. UPDATE appointment a1 JOIN ( SELECT a3.appointmentId diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java index 1fef0e42..a133c29f 100644 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java @@ -90,7 +90,7 @@ class AdoptionServiceTest { void createAdoptionAutoAssignsFirstStaffEmployee() { when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - // resolveAdoptionEmployee filters for staff + when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { Adoption adoption = invocation.getArgument(0); diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java index 71b7005c..cab8e4cb 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -22,7 +22,6 @@ public class AppointmentDTO { private SimpleStringProperty appointmentTime; private SimpleStringProperty appointmentStatus; - // Constructor public AppointmentDTO(int appointmentId, int customerId, String customerName, int petId, String petName, @@ -47,7 +46,6 @@ public class AppointmentDTO { this.appointmentStatus = new SimpleStringProperty(appointmentStatus); } - // Getters public int getAppointmentId() { return appointmentId.get(); } public int getCustomerId() { return customerId.get(); } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 0b8fe447..4de4d212 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -68,7 +68,7 @@ public class AdoptionController { void initialize() { btnEdit.setDisable(true); btnDelete.setDisable(true); - //Enable multiple selection + tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); @@ -91,7 +91,6 @@ public class AdoptionController { displayFilteredAdoptions(newValue); }); - //EventListener for DELETE key tvAdoptions.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (tvAdoptions.getSelectionModel().getSelectedItem() != null) { @@ -109,11 +108,10 @@ public class AdoptionController { @FXML void btnDeleteClicked(ActionEvent event) { - //get selected adoptions + var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems(); if (selectedAdoptions.isEmpty()) return; - //ask user to confirm Alert question = new Alert(Alert.AlertType.CONFIRMATION); question.setHeaderText("Please confirm delete"); String message = selectedAdoptions.size() == 1 @@ -123,7 +121,6 @@ public class AdoptionController { question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); Optional result = question.showAndWait(); - //if confirmed, start deletion if (result.isPresent() && result.get() == ButtonType.OK) { List ids = selectedAdoptions.stream() .map(a -> (long) a.getAdoptionId()) @@ -146,7 +143,6 @@ public class AdoptionController { alert.showAndWait(); } - //refresh display and reset inputs displayAdoptions(); btnDelete.setDisable(true); btnEdit.setDisable(true); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index ba4c05aa..d183918a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -47,7 +47,7 @@ public class AppointmentController { @FXML public void initialize(){ - //Enable multiple selection + tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId")); @@ -66,7 +66,6 @@ public class AppointmentController { txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); } - //EventListener for DELETE key tvAppointments.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (tvAppointments.getSelectionModel().getSelectedItem() != null) { @@ -148,11 +147,10 @@ public class AppointmentController { @FXML void btnDeleteClicked(ActionEvent event){ - //get selected appointments + var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems(); if (selectedAppointments.isEmpty()) return; - //ask user to confirm Alert question = new Alert(Alert.AlertType.CONFIRMATION); question.setHeaderText("Please confirm delete"); String message = selectedAppointments.size() == 1 @@ -162,7 +160,6 @@ public class AppointmentController { question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); java.util.Optional result = question.showAndWait(); - //if confirmed, start deletion if (result.isPresent() && result.get() == ButtonType.OK) { List ids = selectedAppointments.stream() .map(a -> (long) a.getAppointmentId()) @@ -185,7 +182,6 @@ public class AppointmentController { alert.showAndWait(); } - //refresh display loadAppointments(); } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index 56f682c6..67e1073f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -28,7 +28,6 @@ import java.util.Objects; public class AdoptionDialogController { - //FXML elements @FXML private Button btnCancel; @@ -56,11 +55,9 @@ public class AdoptionDialogController { @FXML private Label lblMode; - //Stores if the dialog view is in add/edit mode private String mode = null; private Adoption selectedAdoption = null; - //Adoption statuses private ObservableList statusList = FXCollections.observableArrayList( "Pending", "Completed", "Cancelled" ); @@ -234,7 +231,6 @@ public class AdoptionDialogController { } } - private void closeStage(MouseEvent mouseEvent) { Node node = (Node) mouseEvent.getSource(); Stage stage = (Stage) node.getScene().getWindow(); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index cac4f47e..d21ad756 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -26,10 +26,6 @@ import java.util.Objects; public class AppointmentDialogController { - // ============================ - // FXML - // ============================ - @FXML private Button btnCancel; @FXML private Button btnSave; @@ -47,11 +43,7 @@ public class AppointmentDialogController { @FXML private Label lblAppointmentId; @FXML private Label lblMode; - // ============================ - // DATA - // ============================ - - private String mode = null; // Add | Edit + private String mode = null; private AppointmentDTO selectedAppointment = null; private Long pendingPetSelectionId = null; @@ -60,20 +52,12 @@ public class AppointmentDialogController { "Booked", "Completed", "Cancelled", "Missed" ); - // - // MODE - // - public void setMode(String mode) { this.mode = mode; lblMode.setText(mode + " Appointment"); lblAppointmentId.setVisible(!mode.equals("Add")); } - // - // INITIALIZE - // - @FXML public void initialize() { cbAppointmentStatus.setItems(statusList); @@ -85,14 +69,12 @@ public class AppointmentDialogController { dpAppointmentDate.setValue(LocalDate.now().plusDays(1)); cbAppointmentStatus.setValue("Booked"); - // Hours 9 AM - 5 PM for (int i = 9; i <= 17; i++) { cbHour.getItems().add(i); } cbMinute.getItems().addAll(0, 15, 30, 45); - // Show dropdown labels cbService.setCellFactory(param -> new ListCell<>() { @Override protected void updateItem(DropdownOption option, boolean empty) { @@ -175,10 +157,6 @@ public class AppointmentDialogController { loadEmployees(); } - // - // DISPLAY FOR EDIT - // - public void displayAppointmentDetails(AppointmentDTO appt) { selectedAppointment = appt; @@ -214,10 +192,6 @@ public class AppointmentDialogController { applySelectedEmployee(); } - // - // SAVE - // - private void buttonSaveClicked(MouseEvent e) { if (cbService.getValue() == null || @@ -276,10 +250,6 @@ public class AppointmentDialogController { }).start(); } - // - // UTIL - // - private void closeStage(MouseEvent e) { Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow(); stage.close(); From 0cc4a2bedd5f843d2580f531932e172baa821483 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:45 -0600 Subject: [PATCH 075/137] Pet owner and store --- backend/petshop-api.postman_collection.json | 29 +++++-- .../backend/controller/PetController.java | 3 +- .../petshop/backend/dto/pet/PetRequest.java | 20 +++++ .../petshop/backend/dto/pet/PetResponse.java | 50 +++++++++++- .../java/com/petshop/backend/entity/Pet.java | 24 ++++++ .../backend/repository/PetRepository.java | 12 +-- .../petshop/backend/service/PetService.java | 80 +++++++++++++++++-- .../db/migration/V19__pet_owner_and_store.sql | 23 ++++++ .../backend/service/PetServiceTest.java | 28 ++++--- 9 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 73fed551..1068f4e0 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -564,7 +564,7 @@ "name": "Get All Pets", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/pets", + "url": "{{baseUrl}}/api/v1/pets?status=available&storeId=1", "header": [ { "key": "Content-Type", @@ -585,7 +585,10 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('is page response', function () { pm.expect(json.content).to.be.an('array'); });", + "pm.test('all pets have storeName', function () { json.content.forEach(function(p) { pm.expect(p).to.have.property('storeName'); }); });" ] } } @@ -616,7 +619,10 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('has petId', function () { pm.expect(json.petId).to.be.a('number'); });", + "pm.test('has owner fields', function () { pm.expect(json).to.have.all.keys('petId','petName','petSpecies','petBreed','petAge','petStatus','petPrice','imageUrl','createdAt','updatedAt','customerId','customerName','storeId','storeName'); });" ] } } @@ -671,7 +677,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00\n}", + "raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00,\n \"storeId\": 1\n}", "options": { "raw": { "language": "json" @@ -689,7 +695,11 @@ " pm.response.to.have.status(201);", "});", "var jsonData = pm.response.json();", - "if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);" + "if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);", + "pm.test('has petId', function () { pm.expect(jsonData.petId).to.be.a('number'); });", + "pm.test('has storeId', function () { pm.expect(jsonData.storeId).to.equal(1); });", + "pm.test('has storeName', function () { pm.expect(jsonData.storeName).to.be.a('string'); });", + "pm.test('customerId is null', function () { pm.expect(jsonData.customerId).to.be.null; });" ] } } @@ -713,7 +723,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 375.00\n}", + "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": 1\n}", "options": { "raw": { "language": "json" @@ -729,7 +739,12 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('status is Owned', function () { pm.expect(json.petStatus).to.equal('Owned'); });", + "pm.test('has customerId', function () { pm.expect(json.customerId).to.be.a('number'); });", + "pm.test('has customerName', function () { pm.expect(json.customerName).to.be.a('string'); });", + "pm.test('storeId is null', function () { pm.expect(json.storeId).to.be.null; });" ] } } diff --git a/backend/src/main/java/com/petshop/backend/controller/PetController.java b/backend/src/main/java/com/petshop/backend/controller/PetController.java index 9fb93f0b..60f9a5e4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetController.java @@ -27,8 +27,9 @@ public class PetController { @RequestParam(required = false) String q, @RequestParam(required = false) String species, @RequestParam(required = false) String status, + @RequestParam(required = false) Long storeId, Pageable pageable) { - return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable)); + return ResponseEntity.ok(petService.getAllPets(q, species, status, storeId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index db3f71c9..9a92581a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -23,6 +23,10 @@ public class PetRequest { private BigDecimal petPrice; + private Long customerId; + + private Long storeId; + public String getPetName() { return petName; } @@ -71,6 +75,22 @@ public class PetRequest { this.petPrice = petPrice; } + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java index e3213653..b7113bb1 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java @@ -15,11 +15,15 @@ public class PetResponse { private String imageUrl; private LocalDateTime createdAt; private LocalDateTime updatedAt; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; public PetResponse() { } - public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) { + public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt, Long customerId, String customerName, Long storeId, String storeName) { this.petId = petId; this.petName = petName; this.petSpecies = petSpecies; @@ -30,6 +34,10 @@ public class PetResponse { this.imageUrl = imageUrl; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.customerId = customerId; + this.customerName = customerName; + this.storeId = storeId; + this.storeName = storeName; } public Long getPetId() { @@ -112,17 +120,49 @@ public class PetResponse { this.updatedAt = updatedAt; } + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PetResponse that = (PetResponse) o; - return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName); } @Override public int hashCode() { - return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt); + return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt, customerId, customerName, storeId, storeName); } @Override @@ -138,6 +178,10 @@ public class PetResponse { ", imageUrl='" + imageUrl + '\'' + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + + ", customerId=" + customerId + + ", customerName='" + customerName + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Pet.java b/backend/src/main/java/com/petshop/backend/entity/Pet.java index 8f6a6020..d0b3b3fc 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Pet.java +++ b/backend/src/main/java/com/petshop/backend/entity/Pet.java @@ -38,6 +38,14 @@ public class Pet { @Column(length = 255) private String imageUrl; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customerId") + private Customer customer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "storeId") + private StoreLocation store; + @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -142,6 +150,22 @@ public class Pet { this.updatedAt = updatedAt; } + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index 468c0c9d..d01c2d85 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -18,16 +18,18 @@ public interface PetRepository extends JpaRepository { @Query("SELECT p FROM Pet p WHERE " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + - "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") - Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " + + "(:storeId IS NULL OR p.store.storeId = :storeId)") + Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, Pageable pageable); @Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + - "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species))") - Page searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable); + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + + "(:storeId IS NULL OR p.store.storeId = :storeId)") + Page searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("storeId") Long storeId, Pageable pageable); @Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " + - "(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " + + "(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId OR (LOWER(p.petStatus) = 'owned' AND p.customer.userId = :userId)) AND " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index dc5fa61e..7d038ed4 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -4,12 +4,16 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.pet.PetRequest; import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.entity.Adoption; +import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.StoreRepository; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; @@ -29,15 +33,20 @@ public class PetService { private final PetRepository petRepository; private final AdoptionRepository adoptionRepository; + private final CustomerRepository customerRepository; + private final StoreRepository storeRepository; private final CatalogImageStorageService catalogImageStorageService; - public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CatalogImageStorageService catalogImageStorageService) { + public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CustomerRepository customerRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { this.petRepository = petRepository; this.adoptionRepository = adoptionRepository; + this.customerRepository = customerRepository; + this.storeRepository = storeRepository; this.catalogImageStorageService = catalogImageStorageService; } - public Page getAllPets(String query, String species, String status, Pageable pageable) { + @Transactional(readOnly = true) + public Page getAllPets(String query, String species, String status, Long storeId, Pageable pageable) { String normalizedQuery = normalizeFilter(query); String normalizedSpecies = normalizeFilter(species); String normalizedStatus = normalizeFilter(status); @@ -48,22 +57,23 @@ public class PetService { if (!isAllowedPublicStatus(normalizedStatus)) { return new PageImpl<>(java.util.List.of(), pageable, 0); } - pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable); } else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) { - pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable); + pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, pageable); } else if (viewer.role() == User.Role.CUSTOMER) { if (!isAllowedCustomerStatus(normalizedStatus)) { return new PageImpl<>(java.util.List.of(), pageable, 0); } pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable); } else { - pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable); + pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable); } return pets .map(this::mapToResponse); } + @Transactional(readOnly = true) public PetResponse getPetById(Long id) { Pet pet = petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); @@ -82,6 +92,7 @@ public class PetService { pet.setPetAge(request.getPetAge()); pet.setPetStatus(request.getPetStatus()); pet.setPetPrice(request.getPetPrice()); + applyOwnerAndStore(pet, request); pet = petRepository.save(pet); return mapToResponse(pet); @@ -98,6 +109,7 @@ public class PetService { pet.setPetAge(request.getPetAge()); pet.setPetStatus(request.getPetStatus()); pet.setPetPrice(request.getPetPrice()); + applyOwnerAndStore(pet, request); pet = petRepository.save(pet); return mapToResponse(pet); @@ -161,6 +173,9 @@ public class PetService { if (viewer == null || viewer.userId() == null) { return false; } + if (isOwnedByUser(pet, viewer.userId())) { + return true; + } return isAdoptedByUser(pet, viewer.userId()); } @@ -230,7 +245,7 @@ public class PetService { } private boolean isAllowedCustomerStatus(String status) { - return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status); + return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status); } private String normalizeFilter(String value) { @@ -242,6 +257,8 @@ public class PetService { } private PetResponse mapToResponse(Pet pet) { + Customer customer = pet.getCustomer(); + StoreLocation store = pet.getStore(); return new PetResponse( pet.getPetId(), pet.getPetName(), @@ -252,10 +269,59 @@ public class PetService { pet.getPetPrice(), pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, pet.getCreatedAt(), - pet.getUpdatedAt() + pet.getUpdatedAt(), + customer != null ? customer.getCustomerId() : null, + customer != null ? customer.getFirstName() + " " + customer.getLastName() : null, + store != null ? store.getStoreId() : null, + store != null ? store.getStoreName() : null ); } + private void applyOwnerAndStore(Pet pet, PetRequest request) { + if ("owned".equalsIgnoreCase(request.getPetStatus())) { + if (request.getCustomerId() != null) { + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + pet.setCustomer(customer); + } else { + pet.setCustomer(null); + } + pet.setStore(null); + } else if ("available".equalsIgnoreCase(request.getPetStatus()) || "unadopted".equalsIgnoreCase(request.getPetStatus())) { + if (request.getStoreId() != null) { + StoreLocation store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + pet.setStore(store); + } else { + pet.setStore(null); + } + pet.setCustomer(null); + } else { + if (request.getCustomerId() != null) { + Customer customer = customerRepository.findById(request.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); + pet.setCustomer(customer); + } else { + pet.setCustomer(null); + } + if (request.getStoreId() != null) { + StoreLocation store = storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); + pet.setStore(store); + } else { + pet.setStore(null); + } + } + } + + private boolean isOwnedByUser(Pet pet, Long userId) { + if (!"owned".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) { + return false; + } + Customer customer = pet.getCustomer(); + return customer != null && userId.equals(customer.getUserId()); + } + public record ImagePayload(Resource resource, MediaType mediaType) { } diff --git a/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql b/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql new file mode 100644 index 00000000..918ed375 --- /dev/null +++ b/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql @@ -0,0 +1,23 @@ +ALTER TABLE pet ADD COLUMN customerId BIGINT NULL; +ALTER TABLE pet ADD COLUMN storeId BIGINT NULL; + +ALTER TABLE pet ADD CONSTRAINT fk_pet_customer + FOREIGN KEY (customerId) REFERENCES customer(customerId); +ALTER TABLE pet ADD CONSTRAINT fk_pet_store + FOREIGN KEY (storeId) REFERENCES storeLocation(storeId); + +CREATE INDEX idx_pet_customerId ON pet(customerId); +CREATE INDEX idx_pet_storeId ON pet(storeId); + +UPDATE pet +SET storeId = (SELECT storeId FROM storeLocation ORDER BY storeId ASC LIMIT 1) +WHERE LOWER(petStatus) IN ('available', 'unadopted'); + +UPDATE pet p +JOIN ( + SELECT a.petId, a.customerId + FROM adoption a + WHERE LOWER(a.adoptionStatus) = 'completed' +) latest ON latest.petId = p.petId +SET p.customerId = latest.customerId +WHERE LOWER(p.petStatus) = 'adopted'; diff --git a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java index 9107ebd9..6a80e9ca 100644 --- a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java @@ -6,7 +6,9 @@ import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.security.AppPrincipal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -39,6 +41,12 @@ class PetServiceTest { @Mock private AdoptionRepository adoptionRepository; + @Mock + private CustomerRepository customerRepository; + + @Mock + private StoreRepository storeRepository; + @Mock private CatalogImageStorageService catalogImageStorageService; @@ -54,24 +62,24 @@ class PetServiceTest { void getAllPetsAnonymousReturnsOnlyPublicPets() { Pageable pageable = PageRequest.of(0, 10); Pet availablePet = pet(1L, "Buddy", "Available"); - when(petRepository.searchPublicPets(null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); + when(petRepository.searchPublicPets(null, null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(1, result.getTotalElements()); assertEquals("Buddy", result.getContent().get(0).getPetName()); - verify(petRepository).searchPublicPets(null, null, pageable); - verify(petRepository, never()).searchPets(null, null, null, pageable); + verify(petRepository).searchPublicPets(null, null, null, pageable); + verify(petRepository, never()).searchPets(null, null, null, null, pageable); } @Test void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { Pageable pageable = PageRequest.of(0, 10); - var result = petService.getAllPets(null, null, "Adopted", pageable); + var result = petService.getAllPets(null, null, "Adopted", null, pageable); assertEquals(0, result.getTotalElements()); - verify(petRepository, never()).searchPublicPets(null, null, pageable); + verify(petRepository, never()).searchPublicPets(null, null, null, pageable); } @Test @@ -83,7 +91,7 @@ class PetServiceTest { when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(2, result.getTotalElements()); verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); @@ -95,13 +103,13 @@ class PetServiceTest { setAuthentication(99L, User.Role.ADMIN); Pet availablePet = pet(1L, "Buddy", "Available"); Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.searchPets(null, null, null, pageable)) + when(petRepository.searchPets(null, null, null, null, pageable)) .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(2, result.getTotalElements()); - verify(petRepository).searchPets(null, null, null, pageable); + verify(petRepository).searchPets(null, null, null, null, pageable); } @Test From b2f291f2569c333bfb88dbb3fe3e331d210c7d66 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:49 -0600 Subject: [PATCH 076/137] Update pet desktop --- .../api/dto/pet/PetRequest.java | 18 +++ .../api/dto/pet/PetResponse.java | 36 +++++ .../petshopdesktop/api/endpoints/PetApi.java | 11 +- .../controllers/PetController.java | 17 ++- .../PetDialogController.java | 139 +++++++++++++++++- .../example/petshopdesktop/models/Pet.java | 44 ++++++ .../dialogviews/pet-dialog-view.fxml | 30 +++- .../petshopdesktop/modelviews/pet-view.fxml | 2 + 8 files changed, 286 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java index be71c214..4bb56185 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java @@ -9,6 +9,8 @@ public class PetRequest { private Integer petAge; private String petStatus; private BigDecimal petPrice; + private Long customerId; + private Long storeId; public PetRequest() { } @@ -60,4 +62,20 @@ public class PetRequest { public void setPetPrice(BigDecimal petPrice) { this.petPrice = petPrice; } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java index b1155214..f0961a18 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java @@ -14,6 +14,10 @@ public class PetResponse { private String imageUrl; private LocalDateTime createdAt; private LocalDateTime updatedAt; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; public PetResponse() { } @@ -97,4 +101,36 @@ public class PetResponse { public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java index 92fa28c9..5f69e025 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -24,7 +24,7 @@ public class PetApi { return INSTANCE; } - public List listPets(String query, String species, String status) throws Exception { + public List listPets(String query, String species, String status, Long storeId) throws Exception { String path = "/api/v1/pets?page=0&size=1000"; if (query != null && !query.isEmpty()) { path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); @@ -35,6 +35,9 @@ public class PetApi { if (status != null && !status.isEmpty()) { path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8); } + if (storeId != null) { + path += "&storeId=" + storeId; + } String response = apiClient.getRawResponse(path); PageResponse pageResponse = apiClient.getObjectMapper().readValue( response, @@ -46,8 +49,12 @@ public class PetApi { return pageResponse.getContent(); } + public List listPets(String query, String species, String status) throws Exception { + return listPets(query, species, status, null); + } + public List listPets(String query) throws Exception { - return listPets(query, null, null); + return listPets(query, null, null, null); } public PetResponse createPet(PetRequest request) throws Exception { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index bd76c9ce..005eab1b 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -63,6 +63,12 @@ public class PetController { @FXML private TableColumn colPetStatus; + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colStoreName; + @FXML private TableView tvPets; @@ -156,11 +162,13 @@ public class PetController { colPetAge.setCellValueFactory(new PropertyValueFactory("petAge")); colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus")); colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); + colCustomerName.setCellValueFactory(new PropertyValueFactory("customerName")); + colStoreName.setCellValueFactory(new PropertyValueFactory("storeName")); configureImageColumn(colPetImage); loadSpeciesFilter(); - cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending")); + cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Owned", "Pending")); cbStatusFilter.getSelectionModel().selectFirst(); displayPets(); @@ -316,7 +324,7 @@ public class PetController { } private Pet mapToPet(PetResponse response) { - return new Pet( + Pet pet = new Pet( response.getPetId().intValue(), response.getPetName(), response.getPetSpecies(), @@ -326,6 +334,11 @@ public class PetController { response.getPetPrice().doubleValue(), response.getImageUrl() ); + pet.setCustomerName(response.getCustomerName()); + pet.setStoreName(response.getStoreName()); + pet.setCustomerId(response.getCustomerId() != null ? response.getCustomerId() : 0L); + pet.setStoreId(response.getStoreId() != null ? response.getStoreId() : 0L); + return pet; } private void configureImageColumn(TableColumn column) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java index 78ad836a..afd2f549 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -1,5 +1,6 @@ package org.example.petshopdesktop.controllers.dialogcontrollers; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.EventHandler; @@ -10,8 +11,10 @@ import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.dto.pet.PetRequest; import org.example.petshopdesktop.api.dto.pet.PetResponse; +import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.util.ActivityLogger; @@ -20,6 +23,7 @@ import org.example.petshopdesktop.util.FilePickerSupport; import java.io.File; import java.math.BigDecimal; +import java.util.List; public class PetDialogController { @@ -38,6 +42,12 @@ public class PetDialogController { @FXML private ComboBox cbPetStatus; + @FXML + private ComboBox cbCustomer; + + @FXML + private ComboBox cbStore; + @FXML private Label lblMode; @@ -70,16 +80,54 @@ public class PetDialogController { private String currentImageUrl; private boolean removeImageRequested; + private Long pendingCustomerId = null; + private Long pendingStoreId = null; + private ObservableList statusList = FXCollections.observableArrayList( - "Available", "Adopted" + "Available", "Adopted", "Owned" ); @FXML void initialize() { - cbPetStatus.setItems(statusList); //set status combobox + cbPetStatus.setItems(statusList); + + cbCustomer.setCellFactory(param -> new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + cbCustomer.setButtonCell(new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + + cbStore.setCellFactory(param -> new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + cbStore.setButtonCell(new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + + cbCustomer.setVisible(false); + cbStore.setVisible(false); + + cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> { + boolean isOwned = "Owned".equalsIgnoreCase(newVal); + boolean isAvailable = "Available".equalsIgnoreCase(newVal) || "Unadopted".equalsIgnoreCase(newVal); + cbCustomer.setVisible(isOwned); + cbStore.setVisible(isAvailable); + }); - //Set up mouse handlers for buttons btnSave.setOnMouseClicked(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { @@ -97,6 +145,9 @@ public class PetDialogController { btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); refreshImagePreview(); + + loadCustomers(); + loadStores(); } private void buttonSaveClicked(MouseEvent mouseEvent) { @@ -111,6 +162,10 @@ public class PetDialogController { if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ errorMsg += "Status is required"; } + String selectedStatus = cbPetStatus.getValue(); + if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) { + errorMsg += "Customer is required for Owned status\n"; + } //Check validation (length size) errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); @@ -184,9 +239,81 @@ public class PetDialogController { } request.setPetAge(age); + String status = cbPetStatus.getValue(); + if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) { + request.setCustomerId(cbCustomer.getValue().getId()); + } + if (("Available".equalsIgnoreCase(status) || "Unadopted".equalsIgnoreCase(status)) && cbStore.getValue() != null) { + request.setStoreId(cbStore.getValue().getId()); + } + return request; } + private void loadCustomers() { + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getCustomers(); + Platform.runLater(() -> { + cbCustomer.setItems(FXCollections.observableArrayList(customers)); + applySelectedCustomer(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "PetDialogController.loadCustomers", e, "Loading customers"); + cbCustomer.setDisable(true); + cbCustomer.setPromptText("Unable to load customers"); + }); + } + }).start(); + } + + private void loadStores() { + new Thread(() -> { + try { + List stores = DropdownApi.getInstance().getStores(); + Platform.runLater(() -> { + cbStore.setItems(FXCollections.observableArrayList(stores)); + applySelectedStore(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "PetDialogController.loadStores", e, "Loading stores"); + cbStore.setDisable(true); + cbStore.setPromptText("Unable to load stores"); + }); + } + }).start(); + } + + private void applySelectedCustomer() { + if (pendingCustomerId == null) return; + DropdownOption selected = findOptionById(cbCustomer.getItems(), pendingCustomerId); + if (selected != null) { + cbCustomer.setValue(selected); + pendingCustomerId = null; + } + } + + private void applySelectedStore() { + if (pendingStoreId == null) return; + DropdownOption selected = findOptionById(cbStore.getItems(), pendingStoreId); + if (selected != null) { + cbStore.setValue(selected); + pendingStoreId = null; + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) return null; + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) return option; + } + return null; + } + private void closeStage(MouseEvent mouseEvent) { Node node = (Node) mouseEvent.getSource(); Stage stage = (Stage) node.getScene().getWindow(); @@ -206,14 +333,14 @@ public class PetDialogController { removeImageRequested = false; refreshImagePreview(); - //get the right combobox selection + pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null; + pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null; + for (String status : cbPetStatus.getItems()) { if(status.equals(pet.getPetStatus())){ cbPetStatus.getSelectionModel().select(status); } } - - } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java index fc1723c5..bc7e1a5c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java @@ -13,6 +13,10 @@ public class Pet { private SimpleStringProperty petStatus; private SimpleDoubleProperty petPrice; private SimpleStringProperty imageUrl; + private SimpleStringProperty customerName = new SimpleStringProperty(""); + private SimpleStringProperty storeName = new SimpleStringProperty(""); + private long customerId = 0L; + private long storeId = 0L; public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) { this.petId = new SimpleIntegerProperty(petId); @@ -120,4 +124,44 @@ public class Pet { public SimpleStringProperty imageUrlProperty() { return imageUrl; } + + public String getCustomerName() { + return customerName.get(); + } + + public void setCustomerName(String customerName) { + this.customerName.set(customerName != null ? customerName : ""); + } + + public SimpleStringProperty customerNameProperty() { + return customerName; + } + + public String getStoreName() { + return storeName.get(); + } + + public void setStoreName(String storeName) { + this.storeName.set(storeName != null ? storeName : ""); + } + + public SimpleStringProperty storeNameProperty() { + return storeName; + } + + public long getCustomerId() { + return customerId; + } + + public void setCustomerId(long customerId) { + this.customerId = customerId; + } + + public long getStoreId() { + return storeId; + } + + public void setStoreId(long storeId) { + this.storeId = storeId; + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml index 2f5bd110..f25e113f 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml @@ -13,7 +13,7 @@ - + @@ -153,6 +153,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index c8e77504..d599e010 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -79,6 +79,8 @@ + + From 28e53a437977bc85af219c4055cff3851dda247a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:52 -0600 Subject: [PATCH 077/137] Seed pets and appointments --- .../db/migration/V20__seed_owned_pets.sql | 5 + .../V21__bulk_seed_pets_and_appointments.sql | 144 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V20__seed_owned_pets.sql create mode 100644 backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql diff --git a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql new file mode 100644 index 00000000..50a91e1b --- /dev/null +++ b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql @@ -0,0 +1,5 @@ +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) +VALUES + ('Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, 1), + ('Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, 4), + ('Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, 6); diff --git a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql new file mode 100644 index 00000000..441351ab --- /dev/null +++ b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql @@ -0,0 +1,144 @@ +INSERT INTO customer (firstName, lastName, email) VALUES +('Noah', 'Parker', 'noah@gmail.com'), +('Mia', 'Evans', 'mia@gmail.com'), +('Ethan', 'Scott', 'ethan@gmail.com'), +('Chloe', 'Adams', 'chloe@gmail.com'), +('Lucas', 'Baker', 'lucas@gmail.com'), +('Lily', 'Hall', 'lily@gmail.com'), +('Mason', 'Rivera', 'mason@gmail.com'), +('Ella', 'Mitchell', 'ella@gmail.com'), +('James', 'Carter', 'jcarter@gmail.com'), +('Harper', 'Collins', 'harper@gmail.com'); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) VALUES +('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, 1), +('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, 1), +('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, 1), +('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, 1), +('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, 1), +('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, 2), +('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, 2), +('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, 2), +('Leo', 'Dog', 'Husky', 4, 'Available', 749.00, 2), +('Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, 1), +('Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, 1), +('Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, 3), +('Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, 3), +('Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, 3), +('Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, 1), +('Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, 2), +('Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, 1), +('Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, 3), +('Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, 1), +('Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, 2), +('Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, 1), +('Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, 2), +('Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, 3), +('Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, 1), +('Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, 1); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES +('Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, 1), +('Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, 2), +('Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, 3), +('Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, 4), +('Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, 5); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES +('Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, 6), +('Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, 7), +('Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, 8), +('Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, 9), +('Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, 10); + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-01-10', 'Completed' +FROM pet p WHERE p.petName = 'Shadow' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-01-18', 'Completed' +FROM pet p WHERE p.petName = 'Kitty' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-03', 'Completed' +FROM pet p WHERE p.petName = 'Bruno' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-14', 'Completed' +FROM pet p WHERE p.petName = 'Snowball' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-21', 'Completed' +FROM pet p WHERE p.petName = 'Zeus' AND p.petStatus = 'Adopted'; + +INSERT INTO customer_pet (customer_id, pet_name, species, breed) VALUES +(1, 'Rex', 'Dog', 'German Shepherd'), +(2, 'Whiskers', 'Cat', 'Tabby'), +(3, 'Goldie', 'Dog', 'Golden Retriever'), +(4, 'Midnight', 'Cat', 'Black'), +(5, 'Storm', 'Dog', 'Husky'), +(6, 'Peanut', 'Dog', 'Poodle'), +(7, 'Snowball', 'Cat', 'Persian'), +(8, 'Duke', 'Dog', 'Labrador'), +(9, 'Luna', 'Cat', 'Siamese'), +(10, 'Buster', 'Dog', 'Beagle'), +(11, 'Daisy', 'Dog', 'Corgi'), +(12, 'Cleo', 'Cat', 'Ragdoll'); + +INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) VALUES +(1, 1, '2026-01-10', '09:00:00', 'Completed', 1, 1), +(2, 2, '2026-01-10', '11:00:00', 'Completed', 1, 1), +(3, 3, '2026-01-17', '09:00:00', 'Missed', 1, 1), +(4, 4, '2026-01-17', '14:00:00', 'Completed', 1, 1), +(5, 5, '2026-01-24', '10:00:00', 'Completed', 1, 1), +(1, 6, '2026-01-24', '13:00:00', 'Missed', 1, 1), +(2, 7, '2026-02-07', '09:00:00', 'Completed', 1, 1), +(3, 8, '2026-02-07', '11:00:00', 'Completed', 1, 1), +(1, 9, '2026-01-11', '09:00:00', 'Completed', 1, 2), +(2, 10, '2026-01-11', '11:00:00', 'Missed', 1, 2), +(3, 11, '2026-01-18', '10:00:00', 'Completed', 1, 2), +(4, 12, '2026-01-18', '13:00:00', 'Completed', 1, 2), +(5, 1, '2026-02-01', '09:00:00', 'Completed', 1, 2), +(1, 2, '2026-02-01', '14:00:00', 'Missed', 1, 2), +(2, 3, '2026-02-08', '10:00:00', 'Completed', 1, 2), +(3, 4, '2026-02-08', '13:00:00', 'Completed', 1, 2), +(4, 5, '2026-01-12', '09:00:00', 'Completed', 1, 5), +(5, 6, '2026-01-12', '11:00:00', 'Completed', 1, 5), +(1, 7, '2026-01-19', '09:00:00', 'Missed', 1, 5), +(2, 8, '2026-01-19', '14:00:00', 'Completed', 1, 5), +(3, 9, '2026-02-09', '10:00:00', 'Completed', 1, 5), +(4, 10, '2026-02-09', '13:00:00', 'Completed', 1, 5), +(1, 11, '2026-01-13', '09:00:00', 'Completed', 2, 3), +(2, 12, '2026-01-13', '11:00:00', 'Completed', 2, 3), +(3, 1, '2026-02-10', '09:00:00', 'Missed', 2, 3), +(4, 2, '2026-02-10', '13:00:00', 'Completed', 2, 3), +(1, 3, '2026-01-14', '10:00:00', 'Completed', 3, 4), +(2, 4, '2026-01-14', '13:00:00', 'Completed', 3, 4), +(3, 5, '2026-02-11', '10:00:00', 'Missed', 3, 4), +(4, 6, '2026-02-11', '14:00:00', 'Completed', 3, 4), +(1, 7, '2026-04-15', '09:00:00', 'Booked', 1, 1), +(2, 8, '2026-04-15', '11:00:00', 'Booked', 1, 2), +(3, 9, '2026-04-16', '10:00:00', 'Booked', 1, 5), +(4, 10, '2026-04-17', '09:00:00', 'Booked', 2, 3), +(5, 11, '2026-04-18', '14:00:00', 'Booked', 3, 4); + +INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) +SELECT a.appointmentId, + (((a.appointmentId - 6) % 12) + 1) +FROM appointment a +WHERE a.appointmentId BETWEEN 6 AND 40; From 7ffea449f70b67405ec1fc07ab8c28aebb375014 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 16:25:45 -0600 Subject: [PATCH 078/137] Fix brittle migrations by replacing hardcoded IDs with robust subqueries --- .../db/migration/V20__seed_owned_pets.sql | 9 +- .../V21__bulk_seed_pets_and_appointments.sql | 199 ++++++++++-------- 2 files changed, 113 insertions(+), 95 deletions(-) diff --git a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql index 50a91e1b..5b4f7928 100644 --- a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql +++ b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql @@ -1,5 +1,6 @@ INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) -VALUES - ('Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, 1), - ('Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, 4), - ('Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, 6); +SELECT 'Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, customerId FROM customer WHERE email = 'alex@gmail.com' +UNION ALL +SELECT 'Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'olivia@gmail.com' +UNION ALL +SELECT 'Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, customerId FROM customer WHERE email = 'sophia@gmail.com'; diff --git a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql index 441351ab..8f65a53d 100644 --- a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql +++ b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql @@ -1,3 +1,4 @@ +-- Insert 10 new customers INSERT INTO customer (firstName, lastName, email) VALUES ('Noah', 'Parker', 'noah@gmail.com'), ('Mia', 'Evans', 'mia@gmail.com'), @@ -10,47 +11,51 @@ INSERT INTO customer (firstName, lastName, email) VALUES ('James', 'Carter', 'jcarter@gmail.com'), ('Harper', 'Collins', 'harper@gmail.com'); -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) VALUES -('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, 1), -('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, 1), -('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, 1), -('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, 1), -('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, 1), -('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, 2), -('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, 2), -('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, 2), -('Leo', 'Dog', 'Husky', 4, 'Available', 749.00, 2), -('Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, 1), -('Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, 1), -('Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, 3), -('Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, 3), -('Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, 3), -('Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, 1), -('Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, 2), -('Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, 1), -('Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, 3), -('Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, 1), -('Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, 2), -('Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, 1), -('Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, 2), -('Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, 3), -('Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, 1), -('Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, 1); +-- Insert available pets linked to stores +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) +SELECT 'Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Leo', 'Dog', 'Husky', 4, 'Available', 749.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL +SELECT 'Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL +SELECT 'Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL +SELECT 'Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL +SELECT 'Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL +SELECT 'Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL +SELECT 'Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL +SELECT 'Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch'; -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES -('Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, 1), -('Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, 2), -('Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, 3), -('Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, 4), -('Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, 5); +-- Insert adopted pets linked to customers +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) +SELECT 'Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, customerId FROM customer WHERE email = 'alex@gmail.com' UNION ALL +SELECT 'Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, customerId FROM customer WHERE email = 'emily@gmail.com' UNION ALL +SELECT 'Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, customerId FROM customer WHERE email = 'james@gmail.com' UNION ALL +SELECT 'Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, customerId FROM customer WHERE email = 'olivia@gmail.com' UNION ALL +SELECT 'Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, customerId FROM customer WHERE email = 'william@gmail.com'; -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES -('Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, 6), -('Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, 7), -('Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, 8), -('Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, 9), -('Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, 10); +-- Insert owned pets linked to customers +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) +SELECT 'Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'sophia@gmail.com' UNION ALL +SELECT 'Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, customerId FROM customer WHERE email = 'noah@gmail.com' UNION ALL +SELECT 'Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, customerId FROM customer WHERE email = 'mia@gmail.com' UNION ALL +SELECT 'Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, customerId FROM customer WHERE email = 'ethan@gmail.com' UNION ALL +SELECT 'Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'chloe@gmail.com'; +-- Insert adoptions for the adopted pets INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) SELECT p.petId, p.customerId, (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id @@ -86,59 +91,71 @@ SELECT p.petId, p.customerId, '2026-02-21', 'Completed' FROM pet p WHERE p.petName = 'Zeus' AND p.petStatus = 'Adopted'; -INSERT INTO customer_pet (customer_id, pet_name, species, breed) VALUES -(1, 'Rex', 'Dog', 'German Shepherd'), -(2, 'Whiskers', 'Cat', 'Tabby'), -(3, 'Goldie', 'Dog', 'Golden Retriever'), -(4, 'Midnight', 'Cat', 'Black'), -(5, 'Storm', 'Dog', 'Husky'), -(6, 'Peanut', 'Dog', 'Poodle'), -(7, 'Snowball', 'Cat', 'Persian'), -(8, 'Duke', 'Dog', 'Labrador'), -(9, 'Luna', 'Cat', 'Siamese'), -(10, 'Buster', 'Dog', 'Beagle'), -(11, 'Daisy', 'Dog', 'Corgi'), -(12, 'Cleo', 'Cat', 'Ragdoll'); +-- Insert customer_pet entries +INSERT INTO customer_pet (customer_id, pet_name, species, breed) +SELECT customerId, 'Rex', 'Dog', 'German Shepherd' FROM customer WHERE email = 'alex@gmail.com' UNION ALL +SELECT customerId, 'Whiskers', 'Cat', 'Tabby' FROM customer WHERE email = 'emily@gmail.com' UNION ALL +SELECT customerId, 'Goldie', 'Dog', 'Golden Retriever' FROM customer WHERE email = 'james@gmail.com' UNION ALL +SELECT customerId, 'Midnight', 'Cat', 'Black' FROM customer WHERE email = 'olivia@gmail.com' UNION ALL +SELECT customerId, 'Storm', 'Dog', 'Husky' FROM customer WHERE email = 'william@gmail.com' UNION ALL +SELECT customerId, 'Peanut', 'Dog', 'Poodle' FROM customer WHERE email = 'sophia@gmail.com' UNION ALL +SELECT customerId, 'Snowball', 'Cat', 'Persian' FROM customer WHERE email = 'noah@gmail.com' UNION ALL +SELECT customerId, 'Duke', 'Dog', 'Labrador' FROM customer WHERE email = 'mia@gmail.com' UNION ALL +SELECT customerId, 'Luna', 'Cat', 'Siamese' FROM customer WHERE email = 'ethan@gmail.com' UNION ALL +SELECT customerId, 'Buster', 'Dog', 'Beagle' FROM customer WHERE email = 'chloe@gmail.com' UNION ALL +SELECT customerId, 'Daisy', 'Dog', 'Corgi' FROM customer WHERE email = 'lucas@gmail.com' UNION ALL +SELECT customerId, 'Cleo', 'Cat', 'Ragdoll' FROM customer WHERE email = 'lily@gmail.com'; -INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) VALUES -(1, 1, '2026-01-10', '09:00:00', 'Completed', 1, 1), -(2, 2, '2026-01-10', '11:00:00', 'Completed', 1, 1), -(3, 3, '2026-01-17', '09:00:00', 'Missed', 1, 1), -(4, 4, '2026-01-17', '14:00:00', 'Completed', 1, 1), -(5, 5, '2026-01-24', '10:00:00', 'Completed', 1, 1), -(1, 6, '2026-01-24', '13:00:00', 'Missed', 1, 1), -(2, 7, '2026-02-07', '09:00:00', 'Completed', 1, 1), -(3, 8, '2026-02-07', '11:00:00', 'Completed', 1, 1), -(1, 9, '2026-01-11', '09:00:00', 'Completed', 1, 2), -(2, 10, '2026-01-11', '11:00:00', 'Missed', 1, 2), -(3, 11, '2026-01-18', '10:00:00', 'Completed', 1, 2), -(4, 12, '2026-01-18', '13:00:00', 'Completed', 1, 2), -(5, 1, '2026-02-01', '09:00:00', 'Completed', 1, 2), -(1, 2, '2026-02-01', '14:00:00', 'Missed', 1, 2), -(2, 3, '2026-02-08', '10:00:00', 'Completed', 1, 2), -(3, 4, '2026-02-08', '13:00:00', 'Completed', 1, 2), -(4, 5, '2026-01-12', '09:00:00', 'Completed', 1, 5), -(5, 6, '2026-01-12', '11:00:00', 'Completed', 1, 5), -(1, 7, '2026-01-19', '09:00:00', 'Missed', 1, 5), -(2, 8, '2026-01-19', '14:00:00', 'Completed', 1, 5), -(3, 9, '2026-02-09', '10:00:00', 'Completed', 1, 5), -(4, 10, '2026-02-09', '13:00:00', 'Completed', 1, 5), -(1, 11, '2026-01-13', '09:00:00', 'Completed', 2, 3), -(2, 12, '2026-01-13', '11:00:00', 'Completed', 2, 3), -(3, 1, '2026-02-10', '09:00:00', 'Missed', 2, 3), -(4, 2, '2026-02-10', '13:00:00', 'Completed', 2, 3), -(1, 3, '2026-01-14', '10:00:00', 'Completed', 3, 4), -(2, 4, '2026-01-14', '13:00:00', 'Completed', 3, 4), -(3, 5, '2026-02-11', '10:00:00', 'Missed', 3, 4), -(4, 6, '2026-02-11', '14:00:00', 'Completed', 3, 4), -(1, 7, '2026-04-15', '09:00:00', 'Booked', 1, 1), -(2, 8, '2026-04-15', '11:00:00', 'Booked', 1, 2), -(3, 9, '2026-04-16', '10:00:00', 'Booked', 1, 5), -(4, 10, '2026-04-17', '09:00:00', 'Booked', 2, 3), -(5, 11, '2026-04-18', '14:00:00', 'Booked', 3, 4); +-- Helper function or complex query to seed appointments robustly +-- For simplicity and robustness, I will use individual inserts for the first few and a pattern for the rest +INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-01-10', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-01-10', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-01-17', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-01-17', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-01-24', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-01-24', '13:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-02-07', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-02-07', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-01-11', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-01-11', '11:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-01-18', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'lily@gmail.com'), '2026-01-18', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-02-01', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-02-01', '14:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-02-08', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-02-08', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-01-12', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-01-12', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-01-19', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-01-19', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-02-09', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-02-09', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-01-13', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'lily@gmail.com'), '2026-01-13', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-02-10', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-02-10', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-01-14', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-01-14', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-02-11', '10:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-02-11', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-04-15', '09:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-04-15', '11:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-04-16', '10:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-04-17', '09:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL +SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-04-18', '14:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com'); +-- Re-linking appointments to customer pets using a slightly more robust join +-- This still assumes appointments and customer_pets were inserted in a specific order, +-- but at least it uses current IDs from the database. INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) -SELECT a.appointmentId, - (((a.appointmentId - 6) % 12) + 1) -FROM appointment a -WHERE a.appointmentId BETWEEN 6 AND 40; +SELECT a.appointmentId, cp.customer_pet_id +FROM ( + SELECT appointmentId, ROW_NUMBER() OVER (ORDER BY appointmentId) as row_num + FROM appointment + WHERE appointmentId > (SELECT COALESCE(MAX(appointmentId), 0) FROM (SELECT appointmentId FROM appointment LIMIT 5) t) +) a +JOIN ( + SELECT customer_pet_id, ROW_NUMBER() OVER (ORDER BY customer_pet_id) as row_num + FROM customer_pet +) cp ON ((a.row_num - 1) % 12) + 1 = cp.row_num; From 3e74cdd25e6877acbc18fe513457f4b127e4357f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 19:34:00 -0600 Subject: [PATCH 079/137] Add target DB setup --- backend/docker-compose.target-db.yml | 25 + backend/pom.xml | 51 + .../db/migration/V10__adoption_service.sql | 2 - .../V11__appointment_customer_pet.sql | 7 - .../migration/V12__backfill_user_accounts.sql | 91 - .../V13__remove_debit_payment_method.sql | 3 - .../migration/V14__consolidated_updates.sql | 33 - ...appointment_adoption_employee_required.sql | 61 - .../migration/V16__activate_all_employees.sql | 4 - .../V17__normalize_appointment_pets.sql | 22 - .../V18__past_appointments_missed.sql | 40 - .../db/migration/V19__pet_owner_and_store.sql | 23 - .../db/migration/V1__baseline_schema.sql | 250 --- .../db/migration/V1__target_baseline.sql | 341 +++ .../db/migration/V20__seed_owned_pets.sql | 6 - .../V21__bulk_seed_pets_and_appointments.sql | 161 -- .../resources/db/migration/V2__seed_data.sql | 205 -- ...t_store_and_employee_store_constraints.sql | 19 - .../V4__conversation_mode_and_takeover.sql | 9 - .../db/migration/V5__user_token_version.sql | 2 - .../resources/db/migration/V6__user_phone.sql | 8 - .../V7__employee_customer_phone_cutover.sql | 11 - .../migration/V8__pet_product_image_urls.sql | 5 - .../db/migration/V9__customer_pet.sql | 11 - .../dev/final-target/final_target_schema.sql | 343 +++ .../dev/final-target/final_target_seed.sql | 1861 +++++++++++++++++ 26 files changed, 2621 insertions(+), 973 deletions(-) create mode 100644 backend/docker-compose.target-db.yml delete mode 100644 backend/src/main/resources/db/migration/V10__adoption_service.sql delete mode 100644 backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql delete mode 100644 backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql delete mode 100644 backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql delete mode 100644 backend/src/main/resources/db/migration/V14__consolidated_updates.sql delete mode 100644 backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql delete mode 100644 backend/src/main/resources/db/migration/V16__activate_all_employees.sql delete mode 100644 backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql delete mode 100644 backend/src/main/resources/db/migration/V18__past_appointments_missed.sql delete mode 100644 backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql delete mode 100644 backend/src/main/resources/db/migration/V1__baseline_schema.sql create mode 100644 backend/src/main/resources/db/migration/V1__target_baseline.sql delete mode 100644 backend/src/main/resources/db/migration/V20__seed_owned_pets.sql delete mode 100644 backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql delete mode 100644 backend/src/main/resources/db/migration/V2__seed_data.sql delete mode 100644 backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql delete mode 100644 backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql delete mode 100644 backend/src/main/resources/db/migration/V5__user_token_version.sql delete mode 100644 backend/src/main/resources/db/migration/V6__user_phone.sql delete mode 100644 backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql delete mode 100644 backend/src/main/resources/db/migration/V8__pet_product_image_urls.sql delete mode 100644 backend/src/main/resources/db/migration/V9__customer_pet.sql create mode 100644 backend/src/main/resources/dev/final-target/final_target_schema.sql create mode 100644 backend/src/main/resources/dev/final-target/final_target_seed.sql diff --git a/backend/docker-compose.target-db.yml b/backend/docker-compose.target-db.yml new file mode 100644 index 00000000..03c05785 --- /dev/null +++ b/backend/docker-compose.target-db.yml @@ -0,0 +1,25 @@ +services: + db-target: + image: mysql:8.0 + container_name: petshop-db-target + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: Petstoredb_target + MYSQL_USER: petshop + MYSQL_PASSWORD: petshop + ports: + - "3307:3306" + volumes: + - db_target_data:/var/lib/mysql + - ./src/main/resources/dev/final-target/final_target_schema.sql:/docker-entrypoint-initdb.d/01_final_target_schema.sql:ro + - ./src/main/resources/dev/final-target/final_target_seed.sql:/docker-entrypoint-initdb.d/02_final_target_seed.sql:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 40s + +volumes: + db_target_data: diff --git a/backend/pom.xml b/backend/pom.xml index b511e715..e1c93bd0 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -197,6 +197,57 @@ + + docker-up-target-db + + exec + + + docker + + compose + -f + docker-compose.target-db.yml + up + -d + --wait + db-target + + + + + docker-down-target-db + + exec + + + docker + + compose + -f + docker-compose.target-db.yml + down + -v + --remove-orphans + + + + + docker-logs-target-db + + exec + + + docker + + compose + -f + docker-compose.target-db.yml + logs + db-target + + + diff --git a/backend/src/main/resources/db/migration/V10__adoption_service.sql b/backend/src/main/resources/db/migration/V10__adoption_service.sql deleted file mode 100644 index dd5a57ca..00000000 --- a/backend/src/main/resources/db/migration/V10__adoption_service.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice) -VALUES ('Pet Adoption', 'Schedule a visit to meet and adopt an available pet', 30, 0.00); diff --git a/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql b/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql deleted file mode 100644 index d112fda0..00000000 --- a/backend/src/main/resources/db/migration/V11__appointment_customer_pet.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE IF NOT EXISTS appointment_customer_pet ( - appointment_id BIGINT NOT NULL, - customer_pet_id BIGINT NOT NULL, - PRIMARY KEY (appointment_id, customer_pet_id), - FOREIGN KEY (appointment_id) REFERENCES appointment(appointmentId), - FOREIGN KEY (customer_pet_id) REFERENCES customer_pet(customer_pet_id) -); diff --git a/backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql b/backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql deleted file mode 100644 index 4af69669..00000000 --- a/backend/src/main/resources/db/migration/V12__backfill_user_accounts.sql +++ /dev/null @@ -1,91 +0,0 @@ -INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) -SELECT - CONCAT('customer_', c.customerId) AS username, - '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN c.email IS NOT NULL - AND c.email <> '' - AND (SELECT COUNT(*) FROM customer c2 WHERE c2.email = c.email) = 1 - AND NOT EXISTS (SELECT 1 FROM employee e2 WHERE e2.email = c.email) - AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = c.email) - THEN c.email - ELSE CONCAT('customer_', c.customerId, '@petshop.local') - END AS email, - CONCAT(c.firstName, ' ', c.lastName) AS fullName, - CONCAT('200-000-', LPAD(c.customerId, 4, '0')) AS phone, - 'CUSTOMER' AS role, - FALSE AS active, - 0 AS tokenVersion -FROM customer c -WHERE c.user_id IS NULL - AND NOT EXISTS ( - SELECT 1 - FROM users u - WHERE u.username = CONCAT('customer_', c.customerId) - ); - -INSERT INTO users (username, password, email, fullName, phone, role, active, tokenVersion) -SELECT - CONCAT('employee_', e.employeeId) AS username, - '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq' AS password, - CASE - WHEN e.email IS NOT NULL - AND e.email <> '' - AND (SELECT COUNT(*) FROM employee e2 WHERE e2.email = e.email) = 1 - AND NOT EXISTS (SELECT 1 FROM customer c2 WHERE c2.email = e.email) - AND NOT EXISTS (SELECT 1 FROM users u WHERE u.email = e.email) - THEN e.email - ELSE CONCAT('employee_', e.employeeId, '@petshop.local') - END AS email, - CONCAT(e.firstName, ' ', e.lastName) AS fullName, - CONCAT('300-000-', LPAD(e.employeeId, 4, '0')) AS phone, - CASE - WHEN UPPER(e.role) = 'MANAGER' THEN 'ADMIN' - ELSE 'STAFF' - END AS role, - FALSE AS active, - 0 AS tokenVersion -FROM employee e -WHERE e.user_id IS NULL - AND NOT EXISTS ( - SELECT 1 - FROM users u - WHERE u.username = CONCAT('employee_', e.employeeId) - ); - -UPDATE customer c -JOIN users u ON u.username = CONCAT('customer_', c.customerId) - AND u.role = 'CUSTOMER' -SET c.user_id = u.id -WHERE c.user_id IS NULL; - -UPDATE employee e -JOIN users u ON u.username = CONCAT('employee_', e.employeeId) - AND u.role IN ('STAFF', 'ADMIN') -SET e.user_id = u.id -WHERE e.user_id IS NULL; - -UPDATE users -SET - fullName = CASE - WHEN fullName IS NULL OR fullName = '' THEN username - ELSE fullName - END, - email = CASE - WHEN email IS NULL OR email = '' THEN CONCAT(username, '@petshop.local') - ELSE email - END, - phone = CASE - WHEN phone IS NULL OR phone = '' THEN CONCAT('000-000-', LPAD(id, 4, '0')) - ELSE phone - END, - active = COALESCE(active, TRUE), - tokenVersion = COALESCE(tokenVersion, 0) -WHERE fullName IS NULL - OR fullName = '' - OR email IS NULL - OR email = '' - OR phone IS NULL - OR phone = '' - OR active IS NULL - OR tokenVersion IS NULL; diff --git a/backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql b/backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql deleted file mode 100644 index 874b0205..00000000 --- a/backend/src/main/resources/db/migration/V13__remove_debit_payment_method.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE sale -SET paymentMethod = 'Card' -WHERE LOWER(paymentMethod) = 'debit'; diff --git a/backend/src/main/resources/db/migration/V14__consolidated_updates.sql b/backend/src/main/resources/db/migration/V14__consolidated_updates.sql deleted file mode 100644 index eb0486a8..00000000 --- a/backend/src/main/resources/db/migration/V14__consolidated_updates.sql +++ /dev/null @@ -1,33 +0,0 @@ --- Consolidated Updates: Phone Normalization and Refund Items - --- 1. Create refund_item table -CREATE TABLE IF NOT EXISTS refund_item ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - refund_id BIGINT NOT NULL, - prod_id BIGINT NOT NULL, - quantity INT NOT NULL, - unit_price DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (refund_id) REFERENCES refund(id) ON DELETE CASCADE, - FOREIGN KEY (prod_id) REFERENCES product(prodId) -); - --- 2. Normalize existing phone numbers (MySQL Set-based) -UPDATE users -SET phone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -10, 3), ') ', - SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -7, 3), '-', - SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -4)) -WHERE phone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]'; - -UPDATE supplier -SET supPhone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -10, 3), ') ', - SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -7, 3), '-', - SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -4)) -WHERE supPhone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]'; - -UPDATE storeLocation -SET phone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -10, 3), ') ', - SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -7, 3), '-', - SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -4)) -WHERE phone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]'; diff --git a/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql b/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql deleted file mode 100644 index e931e32a..00000000 --- a/backend/src/main/resources/db/migration/V15__appointment_adoption_employee_required.sql +++ /dev/null @@ -1,61 +0,0 @@ -ALTER TABLE appointment - ADD COLUMN employeeId BIGINT NULL; - -UPDATE appointment a -SET a.employeeId = ( - SELECT es.employeeId - FROM employeeStore es - JOIN employee e ON e.employeeId = es.employeeId - JOIN users u ON u.id = e.user_id - WHERE es.storeId = a.storeId - AND e.isActive = TRUE - AND u.role = 'STAFF' - ORDER BY es.employeeId ASC - LIMIT 1 -) -WHERE a.employeeId IS NULL; - -UPDATE appointment a -SET a.employeeId = ( - SELECT e.employeeId - FROM employee e - JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE - AND u.role = 'STAFF' - ORDER BY e.employeeId ASC - LIMIT 1 -) -WHERE a.employeeId IS NULL; - -ALTER TABLE appointment - ADD CONSTRAINT fk_appointment_employee - FOREIGN KEY (employeeId) REFERENCES employee(employeeId); - -CREATE INDEX idx_appointment_employeeId ON appointment(employeeId); - -ALTER TABLE appointment - MODIFY employeeId BIGINT NOT NULL; - -ALTER TABLE adoption - ADD COLUMN employeeId BIGINT NULL; - -UPDATE adoption a -SET a.employeeId = ( - SELECT e.employeeId - FROM employee e - JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE - AND u.role = 'STAFF' - ORDER BY e.employeeId ASC - LIMIT 1 -) -WHERE a.employeeId IS NULL; - -ALTER TABLE adoption - ADD CONSTRAINT fk_adoption_employee - FOREIGN KEY (employeeId) REFERENCES employee(employeeId); - -CREATE INDEX idx_adoption_employeeId ON adoption(employeeId); - -ALTER TABLE adoption - MODIFY employeeId BIGINT NOT NULL; diff --git a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql b/backend/src/main/resources/db/migration/V16__activate_all_employees.sql deleted file mode 100644 index 314c86c8..00000000 --- a/backend/src/main/resources/db/migration/V16__activate_all_employees.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE users u -SET u.active = TRUE -WHERE u.role IN ('STAFF', 'ADMIN') - AND EXISTS (SELECT 1 FROM employee e WHERE e.user_id = u.id); diff --git a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql b/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql deleted file mode 100644 index 90c2a407..00000000 --- a/backend/src/main/resources/db/migration/V17__normalize_appointment_pets.sql +++ /dev/null @@ -1,22 +0,0 @@ -INSERT INTO customer_pet (customer_id, pet_name, species, breed) -SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed -FROM appointmentPet ap -JOIN appointment a ON a.appointmentId = ap.appointmentId -JOIN pet p ON p.petId = ap.petId -WHERE NOT EXISTS ( - SELECT 1 FROM customer_pet cp - WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName -); - -INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) -SELECT ap.appointmentId, cp.customer_pet_id -FROM appointmentPet ap -JOIN appointment a ON a.appointmentId = ap.appointmentId -JOIN pet p ON p.petId = ap.petId -JOIN customer_pet cp ON cp.customer_id = a.customerId AND cp.pet_name = p.petName -WHERE NOT EXISTS ( - SELECT 1 FROM appointment_customer_pet acp - WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id -); - -DELETE FROM appointmentPet; diff --git a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql b/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql deleted file mode 100644 index 093c6ce3..00000000 --- a/backend/src/main/resources/db/migration/V18__past_appointments_missed.sql +++ /dev/null @@ -1,40 +0,0 @@ -UPDATE appointment -SET appointmentStatus = 'Missed' -WHERE LOWER(appointmentStatus) = 'booked' - AND ( - appointmentDate < CURRENT_DATE - OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME) - ); - -UPDATE appointment a1 -JOIN ( - SELECT a3.appointmentId - FROM appointment a3 - INNER JOIN appointment a4 - ON a4.employeeId = a3.employeeId - AND a4.appointmentDate = a3.appointmentDate - AND a4.appointmentTime = a3.appointmentTime - AND a4.appointmentId < a3.appointmentId - WHERE LOWER(a3.appointmentStatus) NOT IN ('cancelled', 'missed') -) conflicting ON conflicting.appointmentId = a1.appointmentId -SET a1.employeeId = ( - SELECT es.employeeId - FROM employeeStore es - JOIN employee e ON e.employeeId = es.employeeId - JOIN users u ON u.id = e.user_id - WHERE es.storeId = a1.storeId - AND e.isActive = TRUE - AND u.role = 'STAFF' - AND NOT EXISTS ( - SELECT 1 FROM ( - SELECT employeeId, appointmentDate, appointmentTime, appointmentId - FROM appointment - ) snap - WHERE snap.employeeId = es.employeeId - AND snap.appointmentDate = a1.appointmentDate - AND snap.appointmentTime = a1.appointmentTime - AND snap.appointmentId <> a1.appointmentId - ) - ORDER BY es.employeeId ASC - LIMIT 1 -); diff --git a/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql b/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql deleted file mode 100644 index 918ed375..00000000 --- a/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql +++ /dev/null @@ -1,23 +0,0 @@ -ALTER TABLE pet ADD COLUMN customerId BIGINT NULL; -ALTER TABLE pet ADD COLUMN storeId BIGINT NULL; - -ALTER TABLE pet ADD CONSTRAINT fk_pet_customer - FOREIGN KEY (customerId) REFERENCES customer(customerId); -ALTER TABLE pet ADD CONSTRAINT fk_pet_store - FOREIGN KEY (storeId) REFERENCES storeLocation(storeId); - -CREATE INDEX idx_pet_customerId ON pet(customerId); -CREATE INDEX idx_pet_storeId ON pet(storeId); - -UPDATE pet -SET storeId = (SELECT storeId FROM storeLocation ORDER BY storeId ASC LIMIT 1) -WHERE LOWER(petStatus) IN ('available', 'unadopted'); - -UPDATE pet p -JOIN ( - SELECT a.petId, a.customerId - FROM adoption a - WHERE LOWER(a.adoptionStatus) = 'completed' -) latest ON latest.petId = p.petId -SET p.customerId = latest.customerId -WHERE LOWER(p.petStatus) = 'adopted'; diff --git a/backend/src/main/resources/db/migration/V1__baseline_schema.sql b/backend/src/main/resources/db/migration/V1__baseline_schema.sql deleted file mode 100644 index ae1ca009..00000000 --- a/backend/src/main/resources/db/migration/V1__baseline_schema.sql +++ /dev/null @@ -1,250 +0,0 @@ --- Create Tables - -CREATE TABLE IF NOT EXISTS storeLocation ( - storeId BIGINT AUTO_INCREMENT PRIMARY KEY, - storeName VARCHAR(100) NOT NULL, - address VARCHAR(255) NOT NULL, - phone VARCHAR(20) NOT NULL, - email VARCHAR(100) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS employee ( - employeeId BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NULL, - firstName VARCHAR(50) NOT NULL, - lastName VARCHAR(50) NOT NULL, - email VARCHAR(100) NOT NULL, - phone VARCHAR(20) NOT NULL, - role VARCHAR(50) NOT NULL, - isActive BOOLEAN DEFAULT TRUE NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT uk_employee_user_id UNIQUE (user_id) -); - -CREATE TABLE IF NOT EXISTS employeeStore ( - employeeId BIGINT NOT NULL, - storeId BIGINT NOT NULL, - PRIMARY KEY (employeeId, storeId), - FOREIGN KEY (employeeId) REFERENCES employee(employeeId), - FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) -); - -CREATE TABLE IF NOT EXISTS customer ( - customerId BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NULL, - firstName VARCHAR(50) NOT NULL, - lastName VARCHAR(50) NOT NULL, - email VARCHAR(100) NOT NULL, - phone VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT uk_customer_user_id UNIQUE (user_id) -); - -CREATE TABLE IF NOT EXISTS pet ( - petId BIGINT AUTO_INCREMENT PRIMARY KEY, - petName VARCHAR(50) NOT NULL, - petSpecies VARCHAR(50) NOT NULL, - petBreed VARCHAR(50) NOT NULL, - petAge INT NOT NULL, - petStatus VARCHAR(20) NOT NULL, - petPrice DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS adoption ( - adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY, - petId BIGINT NOT NULL, - customerId BIGINT NOT NULL, - adoptionDate DATE NOT NULL, - adoptionStatus VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (petId) REFERENCES pet(petId), - FOREIGN KEY (customerId) REFERENCES customer(customerId) -); - -CREATE TABLE IF NOT EXISTS supplier ( - supId BIGINT AUTO_INCREMENT PRIMARY KEY, - supCompany VARCHAR(100) NOT NULL, - supContactFirstName VARCHAR(50) NOT NULL, - supContactLastName VARCHAR(50) NOT NULL, - supEmail VARCHAR(100) NOT NULL, - supPhone VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS category ( - categoryId BIGINT AUTO_INCREMENT PRIMARY KEY, - categoryName VARCHAR(100) NOT NULL, - categoryType VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS product ( - prodId BIGINT AUTO_INCREMENT PRIMARY KEY, - prodName VARCHAR(100) NOT NULL, - prodPrice DECIMAL(10, 2) NOT NULL, - categoryId BIGINT NOT NULL, - prodDesc TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (categoryId) REFERENCES category(categoryId) -); - -CREATE TABLE IF NOT EXISTS productSupplier ( - supId BIGINT NOT NULL, - prodId BIGINT NOT NULL, - cost DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (supId, prodId), - FOREIGN KEY (supId) REFERENCES supplier(supId), - FOREIGN KEY (prodId) REFERENCES product(prodId) -); - -CREATE TABLE IF NOT EXISTS inventory ( - inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY, - prodId BIGINT NOT NULL, - quantity INT DEFAULT 0 NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (prodId) REFERENCES product(prodId) -); - -CREATE TABLE IF NOT EXISTS service ( - serviceId BIGINT AUTO_INCREMENT PRIMARY KEY, - serviceName VARCHAR(100) NOT NULL, - serviceDesc TEXT, - serviceDuration INT NOT NULL, - servicePrice DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS appointment ( - appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY, - serviceId BIGINT NOT NULL, - customerId BIGINT NOT NULL, - appointmentDate DATE NOT NULL, - appointmentTime TIME NOT NULL, - appointmentStatus VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (serviceId) REFERENCES service(serviceId), - FOREIGN KEY (customerId) REFERENCES customer(customerId) -); - -CREATE TABLE IF NOT EXISTS appointmentPet ( - appointmentId BIGINT NOT NULL, - petId BIGINT NOT NULL, - PRIMARY KEY (appointmentId, petId), - FOREIGN KEY (appointmentId) REFERENCES appointment(appointmentId), - FOREIGN KEY (petId) REFERENCES pet(petId) -); - -CREATE TABLE IF NOT EXISTS sale ( - saleId BIGINT AUTO_INCREMENT PRIMARY KEY, - saleDate DATETIME NOT NULL, - totalAmount DECIMAL(10, 2) NOT NULL, - paymentMethod VARCHAR(50) NOT NULL, - employeeId BIGINT NOT NULL, - storeId BIGINT NOT NULL, - customerId BIGINT NULL, - isRefund BOOLEAN DEFAULT FALSE NOT NULL, - originalSaleId BIGINT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (employeeId) REFERENCES employee(employeeId), - FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), - FOREIGN KEY (customerId) REFERENCES customer(customerId), - FOREIGN KEY (originalSaleId) REFERENCES sale(saleId) -); - -CREATE TABLE IF NOT EXISTS saleItem ( - saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY, - saleId BIGINT NOT NULL, - prodId BIGINT NOT NULL, - quantity INT NOT NULL, - unitPrice DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (saleId) REFERENCES sale(saleId), - FOREIGN KEY (prodId) REFERENCES product(prodId) -); - -CREATE TABLE IF NOT EXISTS purchaseOrder ( - purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY, - supId BIGINT NOT NULL, - orderDate DATE NOT NULL, - status VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (supId) REFERENCES supplier(supId) -); - -CREATE TABLE IF NOT EXISTS activityLog ( - logId BIGINT AUTO_INCREMENT PRIMARY KEY, - employeeId BIGINT NOT NULL, - activity TEXT NOT NULL, - logTimestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY (employeeId) REFERENCES employee(employeeId) -); - -CREATE TABLE IF NOT EXISTS users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - password VARCHAR(255) NOT NULL, - email VARCHAR(100) UNIQUE, - fullName VARCHAR(100), - avatarUrl VARCHAR(255), - role VARCHAR(20) NOT NULL, - active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS refund ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - saleId BIGINT NOT NULL, - customerId BIGINT NOT NULL, - amount DECIMAL(10, 2) NOT NULL, - reason VARCHAR(500) NOT NULL, - status VARCHAR(20) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (saleId) REFERENCES sale(saleId), - FOREIGN KEY (customerId) REFERENCES customer(customerId) -); - -CREATE TABLE IF NOT EXISTS conversation ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - customerId BIGINT NOT NULL, - staffId BIGINT, - status VARCHAR(20) NOT NULL DEFAULT 'OPEN', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (customerId) REFERENCES customer(customerId), - FOREIGN KEY (staffId) REFERENCES users(id) -); - -CREATE TABLE IF NOT EXISTS message ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - conversationId BIGINT NOT NULL, - senderId BIGINT NOT NULL, - content TEXT NOT NULL, - timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - isRead BOOLEAN NOT NULL DEFAULT FALSE, - FOREIGN KEY (conversationId) REFERENCES conversation(id), - FOREIGN KEY (senderId) REFERENCES users(id) -); - --- Add foreign keys for user_id linkage -ALTER TABLE employee ADD CONSTRAINT fk_employee_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; -ALTER TABLE customer ADD CONSTRAINT fk_customer_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/backend/src/main/resources/db/migration/V1__target_baseline.sql b/backend/src/main/resources/db/migration/V1__target_baseline.sql new file mode 100644 index 00000000..d287c8e1 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__target_baseline.sql @@ -0,0 +1,341 @@ + +CREATE TABLE IF NOT EXISTS storeLocation ( + storeId BIGINT AUTO_INCREMENT PRIMARY KEY, + storeName VARCHAR(100) NOT NULL, + address VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR(100) NOT NULL, + imageUrl VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NULL UNIQUE, + password VARCHAR(255) NULL, + email VARCHAR(100) NULL UNIQUE, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL, + fullName VARCHAR(100) NULL, + phone VARCHAR(20) NULL, + avatarUrl VARCHAR(255) NULL, + role VARCHAR(20) NOT NULL, + staffRole VARCHAR(50) NULL, + primaryStoreId BIGINT NULL, + loyaltyPoints INT NOT NULL DEFAULT 0, + active BOOLEAN NOT NULL DEFAULT TRUE, + tokenVersion INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_users_primary_store FOREIGN KEY (primaryStoreId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS supplier ( + supId BIGINT AUTO_INCREMENT PRIMARY KEY, + supCompany VARCHAR(100) NOT NULL, + supContactFirstName VARCHAR(50) NOT NULL, + supContactLastName VARCHAR(50) NOT NULL, + supEmail VARCHAR(100) NOT NULL, + supPhone VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS category ( + categoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + categoryName VARCHAR(100) NOT NULL, + categoryType VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_category_name_type UNIQUE (categoryName, categoryType) +); + +CREATE TABLE IF NOT EXISTS service ( + serviceId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceName VARCHAR(100) NOT NULL, + serviceDesc TEXT NULL, + serviceDuration INT NOT NULL, + servicePrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS service_species ( + serviceId BIGINT NOT NULL, + species VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (serviceId, species), + CONSTRAINT fk_service_species_service FOREIGN KEY (serviceId) REFERENCES service(serviceId) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS product ( + prodId BIGINT AUTO_INCREMENT PRIMARY KEY, + prodName VARCHAR(100) NOT NULL, + prodPrice DECIMAL(10, 2) NOT NULL, + categoryId BIGINT NOT NULL, + prodDesc TEXT NULL, + imageUrl VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_product_category FOREIGN KEY (categoryId) REFERENCES category(categoryId) +); + +CREATE TABLE IF NOT EXISTS inventory ( + inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + storeId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_inventory_store_product UNIQUE (storeId, prodId), + CONSTRAINT fk_inventory_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_inventory_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS productSupplier ( + supId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + cost DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (supId, prodId), + CONSTRAINT fk_product_supplier_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), + CONSTRAINT fk_product_supplier_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS purchaseOrder ( + purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY, + supId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + orderDate DATE NOT NULL, + status VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_purchase_order_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), + CONSTRAINT fk_purchase_order_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) +); + +CREATE TABLE IF NOT EXISTS coupon ( + couponId BIGINT AUTO_INCREMENT PRIMARY KEY, + couponCode VARCHAR(50) NOT NULL, + discountType VARCHAR(20) NOT NULL, + discountValue DECIMAL(10, 2) NOT NULL, + minOrderAmount DECIMAL(10, 2) NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, + startsAt DATETIME NULL, + endsAt DATETIME NULL, + usageLimit INT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_coupon_code UNIQUE (couponCode) +); + +CREATE TABLE IF NOT EXISTS pet ( + petId BIGINT AUTO_INCREMENT PRIMARY KEY, + petName VARCHAR(50) NOT NULL, + petSpecies VARCHAR(50) NOT NULL, + petBreed VARCHAR(50) NULL, + petAge INT NULL, + petStatus VARCHAR(20) NOT NULL, + petPrice DECIMAL(10, 2) NULL, + imageUrl VARCHAR(255) NULL, + ownerUserId BIGINT NULL, + storeId BIGINT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_pet_owner_user FOREIGN KEY (ownerUserId) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_pet_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS appointment ( + appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceId BIGINT NOT NULL, + petId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + employeeId BIGINT NOT NULL, + appointmentDate DATE NOT NULL, + appointmentTime TIME NOT NULL, + appointmentStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_appointment_service FOREIGN KEY (serviceId) REFERENCES service(serviceId), + CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId), + CONSTRAINT fk_appointment_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_appointment_employee FOREIGN KEY (employeeId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS adoption ( + adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY, + petId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + employeeId BIGINT NOT NULL, + sourceStoreId BIGINT NOT NULL, + adoptionDate DATE NOT NULL, + adoptionStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_adoption_pet FOREIGN KEY (petId) REFERENCES pet(petId), + CONSTRAINT fk_adoption_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_adoption_employee FOREIGN KEY (employeeId) REFERENCES users(id), + CONSTRAINT fk_adoption_source_store FOREIGN KEY (sourceStoreId) REFERENCES storeLocation(storeId) +); + +CREATE TABLE IF NOT EXISTS cart ( + cartId BIGINT AUTO_INCREMENT PRIMARY KEY, + userId BIGINT NOT NULL, + storeId BIGINT NULL, + couponId BIGINT NULL, + cartStatus VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + subtotalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + discountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + totalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_cart_user FOREIGN KEY (userId) REFERENCES users(id), + CONSTRAINT fk_cart_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, + CONSTRAINT fk_cart_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS cart_item ( + cartItemId BIGINT AUTO_INCREMENT PRIMARY KEY, + cartId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL, + unitPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_cart_item_cart FOREIGN KEY (cartId) REFERENCES cart(cartId) ON DELETE CASCADE, + CONSTRAINT fk_cart_item_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS sale ( + saleId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleDate DATETIME NOT NULL, + totalAmount DECIMAL(10, 2) NOT NULL, + paymentMethod VARCHAR(50) NOT NULL, + employeeId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + customerId BIGINT NULL, + isRefund BOOLEAN NOT NULL DEFAULT FALSE, + originalSaleId BIGINT NULL, + channel VARCHAR(20) NOT NULL DEFAULT 'IN_STORE', + cartId BIGINT NULL, + couponId BIGINT NULL, + subtotalAmount DECIMAL(10, 2) NULL, + couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + pointsEarned INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id), + CONSTRAINT fk_sale_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_sale_customer FOREIGN KEY (customerId) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_sale_original_sale FOREIGN KEY (originalSaleId) REFERENCES sale(saleId), + CONSTRAINT fk_sale_cart FOREIGN KEY (cartId) REFERENCES cart(cartId) ON DELETE SET NULL, + CONSTRAINT fk_sale_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS saleItem ( + saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL, + unitPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_sale_item_sale FOREIGN KEY (saleId) REFERENCES sale(saleId) ON DELETE CASCADE, + CONSTRAINT fk_sale_item_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS refund ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + reason VARCHAR(500) NOT NULL, + status VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_refund_sale FOREIGN KEY (saleId) REFERENCES sale(saleId), + CONSTRAINT fk_refund_customer FOREIGN KEY (customerId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS refund_item ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + refund_id BIGINT NOT NULL, + prod_id BIGINT NOT NULL, + quantity INT NOT NULL, + unit_price DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_refund_item_refund FOREIGN KEY (refund_id) REFERENCES refund(id) ON DELETE CASCADE, + CONSTRAINT fk_refund_item_product FOREIGN KEY (prod_id) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS conversation ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + customerId BIGINT NOT NULL, + staffId BIGINT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', + mode VARCHAR(20) NOT NULL DEFAULT 'AUTOMATED', + humanRequestedAt TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_conversation_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_conversation_staff FOREIGN KEY (staffId) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS message ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + conversationId BIGINT NOT NULL, + senderId BIGINT NOT NULL, + content TEXT NULL, + attachmentUrl VARCHAR(255) NULL, + attachmentName VARCHAR(255) NULL, + attachmentMimeType VARCHAR(100) NULL, + attachmentSizeBytes BIGINT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + isRead BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT fk_message_conversation FOREIGN KEY (conversationId) REFERENCES conversation(id) ON DELETE CASCADE, + CONSTRAINT fk_message_sender FOREIGN KEY (senderId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS activityLog ( + logId BIGINT AUTO_INCREMENT PRIMARY KEY, + userId BIGINT NOT NULL, + storeId BIGINT NULL, + activity TEXT NOT NULL, + logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id), + CONSTRAINT fk_activity_log_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE INDEX idx_users_primary_store ON users(primaryStoreId); +CREATE INDEX idx_users_role ON users(role); +CREATE INDEX idx_users_name ON users(lastName, firstName); +CREATE INDEX idx_service_species_species ON service_species(species); +CREATE INDEX idx_inventory_store ON inventory(storeId); +CREATE INDEX idx_inventory_product ON inventory(prodId); +CREATE INDEX idx_purchase_order_store ON purchaseOrder(storeId); +CREATE INDEX idx_pet_owner_user ON pet(ownerUserId); +CREATE INDEX idx_pet_store ON pet(storeId); +CREATE INDEX idx_pet_species ON pet(petSpecies); +CREATE INDEX idx_pet_name ON pet(petName); +CREATE INDEX idx_appointment_store ON appointment(storeId); +CREATE INDEX idx_appointment_employee ON appointment(employeeId); +CREATE INDEX idx_appointment_customer ON appointment(customerId); +CREATE INDEX idx_appointment_pet ON appointment(petId); +CREATE INDEX idx_appointment_date_status ON appointment(appointmentDate, appointmentStatus); +CREATE INDEX idx_adoption_store ON adoption(sourceStoreId); +CREATE INDEX idx_adoption_employee ON adoption(employeeId); +CREATE INDEX idx_sale_store ON sale(storeId); +CREATE INDEX idx_sale_employee ON sale(employeeId); +CREATE INDEX idx_sale_customer ON sale(customerId); +CREATE INDEX idx_sale_date ON sale(saleDate); +CREATE INDEX idx_cart_user ON cart(userId); +CREATE INDEX idx_conversation_customer ON conversation(customerId); +CREATE INDEX idx_conversation_staff ON conversation(staffId); +CREATE INDEX idx_activity_log_store ON activityLog(storeId); diff --git a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql deleted file mode 100644 index 5b4f7928..00000000 --- a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) -SELECT 'Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, customerId FROM customer WHERE email = 'alex@gmail.com' -UNION ALL -SELECT 'Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'olivia@gmail.com' -UNION ALL -SELECT 'Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, customerId FROM customer WHERE email = 'sophia@gmail.com'; diff --git a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql deleted file mode 100644 index 8f65a53d..00000000 --- a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql +++ /dev/null @@ -1,161 +0,0 @@ --- Insert 10 new customers -INSERT INTO customer (firstName, lastName, email) VALUES -('Noah', 'Parker', 'noah@gmail.com'), -('Mia', 'Evans', 'mia@gmail.com'), -('Ethan', 'Scott', 'ethan@gmail.com'), -('Chloe', 'Adams', 'chloe@gmail.com'), -('Lucas', 'Baker', 'lucas@gmail.com'), -('Lily', 'Hall', 'lily@gmail.com'), -('Mason', 'Rivera', 'mason@gmail.com'), -('Ella', 'Mitchell', 'ella@gmail.com'), -('James', 'Carter', 'jcarter@gmail.com'), -('Harper', 'Collins', 'harper@gmail.com'); - --- Insert available pets linked to stores -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) -SELECT 'Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Leo', 'Dog', 'Husky', 4, 'Available', 749.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL -SELECT 'Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL -SELECT 'Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL -SELECT 'Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL -SELECT 'Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, storeId FROM storeLocation WHERE storeName = 'North Branch' UNION ALL -SELECT 'Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, storeId FROM storeLocation WHERE storeName = 'West Side Store' UNION ALL -SELECT 'Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch' UNION ALL -SELECT 'Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, storeId FROM storeLocation WHERE storeName = 'Downtown Branch'; - --- Insert adopted pets linked to customers -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) -SELECT 'Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, customerId FROM customer WHERE email = 'alex@gmail.com' UNION ALL -SELECT 'Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, customerId FROM customer WHERE email = 'emily@gmail.com' UNION ALL -SELECT 'Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, customerId FROM customer WHERE email = 'james@gmail.com' UNION ALL -SELECT 'Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, customerId FROM customer WHERE email = 'olivia@gmail.com' UNION ALL -SELECT 'Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, customerId FROM customer WHERE email = 'william@gmail.com'; - --- Insert owned pets linked to customers -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) -SELECT 'Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'sophia@gmail.com' UNION ALL -SELECT 'Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, customerId FROM customer WHERE email = 'noah@gmail.com' UNION ALL -SELECT 'Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, customerId FROM customer WHERE email = 'mia@gmail.com' UNION ALL -SELECT 'Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, customerId FROM customer WHERE email = 'ethan@gmail.com' UNION ALL -SELECT 'Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, customerId FROM customer WHERE email = 'chloe@gmail.com'; - --- Insert adoptions for the adopted pets -INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) -SELECT p.petId, p.customerId, - (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), - '2026-01-10', 'Completed' -FROM pet p WHERE p.petName = 'Shadow' AND p.petStatus = 'Adopted'; - -INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) -SELECT p.petId, p.customerId, - (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), - '2026-01-18', 'Completed' -FROM pet p WHERE p.petName = 'Kitty' AND p.petStatus = 'Adopted'; - -INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) -SELECT p.petId, p.customerId, - (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), - '2026-02-03', 'Completed' -FROM pet p WHERE p.petName = 'Bruno' AND p.petStatus = 'Adopted'; - -INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) -SELECT p.petId, p.customerId, - (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), - '2026-02-14', 'Completed' -FROM pet p WHERE p.petName = 'Snowball' AND p.petStatus = 'Adopted'; - -INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) -SELECT p.petId, p.customerId, - (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id - WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), - '2026-02-21', 'Completed' -FROM pet p WHERE p.petName = 'Zeus' AND p.petStatus = 'Adopted'; - --- Insert customer_pet entries -INSERT INTO customer_pet (customer_id, pet_name, species, breed) -SELECT customerId, 'Rex', 'Dog', 'German Shepherd' FROM customer WHERE email = 'alex@gmail.com' UNION ALL -SELECT customerId, 'Whiskers', 'Cat', 'Tabby' FROM customer WHERE email = 'emily@gmail.com' UNION ALL -SELECT customerId, 'Goldie', 'Dog', 'Golden Retriever' FROM customer WHERE email = 'james@gmail.com' UNION ALL -SELECT customerId, 'Midnight', 'Cat', 'Black' FROM customer WHERE email = 'olivia@gmail.com' UNION ALL -SELECT customerId, 'Storm', 'Dog', 'Husky' FROM customer WHERE email = 'william@gmail.com' UNION ALL -SELECT customerId, 'Peanut', 'Dog', 'Poodle' FROM customer WHERE email = 'sophia@gmail.com' UNION ALL -SELECT customerId, 'Snowball', 'Cat', 'Persian' FROM customer WHERE email = 'noah@gmail.com' UNION ALL -SELECT customerId, 'Duke', 'Dog', 'Labrador' FROM customer WHERE email = 'mia@gmail.com' UNION ALL -SELECT customerId, 'Luna', 'Cat', 'Siamese' FROM customer WHERE email = 'ethan@gmail.com' UNION ALL -SELECT customerId, 'Buster', 'Dog', 'Beagle' FROM customer WHERE email = 'chloe@gmail.com' UNION ALL -SELECT customerId, 'Daisy', 'Dog', 'Corgi' FROM customer WHERE email = 'lucas@gmail.com' UNION ALL -SELECT customerId, 'Cleo', 'Cat', 'Ragdoll' FROM customer WHERE email = 'lily@gmail.com'; - --- Helper function or complex query to seed appointments robustly --- For simplicity and robustness, I will use individual inserts for the first few and a pattern for the rest -INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-01-10', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-01-10', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-01-17', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-01-17', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-01-24', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-01-24', '13:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-02-07', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-02-07', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-01-11', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-01-11', '11:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-01-18', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'lily@gmail.com'), '2026-01-18', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-02-01', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-02-01', '14:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-02-08', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-02-08', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-01-12', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-01-12', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-01-19', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-01-19', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-02-09', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-02-09', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-01-13', '09:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'lily@gmail.com'), '2026-01-13', '11:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'alex@gmail.com'), '2026-02-10', '09:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'emily@gmail.com'), '2026-02-10', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'james@gmail.com'), '2026-01-14', '10:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'olivia@gmail.com'), '2026-01-14', '13:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'william@gmail.com'), '2026-02-11', '10:00:00', 'Missed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'sophia@gmail.com'), '2026-02-11', '14:00:00', 'Completed', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Pet Grooming'), (SELECT customerId FROM customer WHERE email = 'noah@gmail.com'), '2026-04-15', '09:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'john@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Nail Trimming'), (SELECT customerId FROM customer WHERE email = 'mia@gmail.com'), '2026-04-15', '11:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'sara@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Bath and Brush'), (SELECT customerId FROM customer WHERE email = 'ethan@gmail.com'), '2026-04-16', '10:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'Downtown Branch'), (SELECT employeeId FROM employee WHERE email = 'david@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Veterinary Checkup'), (SELECT customerId FROM customer WHERE email = 'chloe@gmail.com'), '2026-04-17', '09:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'North Branch'), (SELECT employeeId FROM employee WHERE email = 'michael@petshop.com') UNION ALL -SELECT (SELECT serviceId FROM service WHERE serviceName = 'Teeth Cleaning'), (SELECT customerId FROM customer WHERE email = 'lucas@gmail.com'), '2026-04-18', '14:00:00', 'Booked', (SELECT storeId FROM storeLocation WHERE storeName = 'West Side Store'), (SELECT employeeId FROM employee WHERE email = 'lisa@petshop.com'); - --- Re-linking appointments to customer pets using a slightly more robust join --- This still assumes appointments and customer_pets were inserted in a specific order, --- but at least it uses current IDs from the database. -INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) -SELECT a.appointmentId, cp.customer_pet_id -FROM ( - SELECT appointmentId, ROW_NUMBER() OVER (ORDER BY appointmentId) as row_num - FROM appointment - WHERE appointmentId > (SELECT COALESCE(MAX(appointmentId), 0) FROM (SELECT appointmentId FROM appointment LIMIT 5) t) -) a -JOIN ( - SELECT customer_pet_id, ROW_NUMBER() OVER (ORDER BY customer_pet_id) as row_num - FROM customer_pet -) cp ON ((a.row_num - 1) % 12) + 1 = cp.row_num; diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql deleted file mode 100644 index 5e8d3fb6..00000000 --- a/backend/src/main/resources/db/migration/V2__seed_data.sql +++ /dev/null @@ -1,205 +0,0 @@ --- Insert Sample Data - -INSERT INTO storeLocation (storeName, address, phone, email) -VALUES -('Downtown Branch', '123 Main St', '123-456-7890', 'downtown@petshop.com'), -('North Branch', '456 North Ave', '987-654-3210', 'north@petshop.com'), -('West Side Store', '789 West Blvd', '555-123-4567', 'westside@petshop.com'), -('East End Shop', '321 East Road', '555-987-6543', 'eastend@petshop.com'), -('South Mall Location', '654 South Plaza', '555-246-8135', 'southmall@petshop.com'); - -INSERT INTO employee (firstName, lastName, email, phone, role, isActive) -VALUES -('John', 'Doe', 'john@petshop.com', '111-222-3333', 'Manager', TRUE), -('Sara', 'Smith', 'sara@petshop.com', '444-555-6666', 'Staff', TRUE), -('Michael', 'Johnson', 'michael@petshop.com', '222-333-4444', 'Groomer', TRUE), -('Lisa', 'Williams', 'lisa@petshop.com', '333-444-5555', 'Staff', TRUE), -('David', 'Brown', 'david@petshop.com', '555-666-7777', 'Veterinarian', TRUE), -('Emma', 'Davis', 'emma@petshop.com', '666-777-8888', 'Manager', FALSE); - -INSERT INTO employeeStore (employeeId, storeId) -VALUES -(1, 1), -(2, 1), -(2, 2), -(3, 2), -(4, 3), -(5, 1), -(5, 4), -(6, 5); - -INSERT INTO customer (firstName, lastName, email, phone) -VALUES -('Alex', 'Brown', 'alex@gmail.com', '777-888-9999'), -('Emily', 'Clark', 'emily@gmail.com', '666-555-4444'), -('James', 'Wilson', 'james@gmail.com', '888-999-0000'), -('Olivia', 'Martinez', 'olivia@gmail.com', '999-000-1111'), -('William', 'Anderson', 'william@gmail.com', '000-111-2222'), -('Sophia', 'Taylor', 'sophia@gmail.com', '111-222-3333'); - -INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice) -VALUES -('Buddy', 'Dog', 'Labrador', 2, 'Available', 500.00), -('Milo', 'Cat', 'Persian', 1, 'Available', 300.00), -('Charlie', 'Dog', 'Golden Retriever', 3, 'Available', 550.00), -('Luna', 'Cat', 'Siamese', 2, 'Adopted', 350.00), -('Max', 'Dog', 'Beagle', 1, 'Available', 450.00), -('Bella', 'Cat', 'Maine Coon', 4, 'Available', 400.00); - -INSERT INTO adoption (petId, customerId, adoptionDate, adoptionStatus) -VALUES -(1, 1, '2026-01-15', 'Completed'), -(4, 3, '2026-01-20', 'Completed'), -(2, 2, '2026-01-25', 'Pending'), -(5, 4, '2026-02-01', 'Completed'), -(6, 5, '2026-02-02', 'Pending'); - -INSERT INTO supplier (supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) -VALUES -('PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '888-111-2222'), -('Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '888-222-3333'), -('Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '888-333-4444'), -('Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '888-444-5555'), -('Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '888-555-6666'); - -INSERT INTO category (categoryName, categoryType) -VALUES -('Dog Food', 'Product'), -('Cat Toys', 'Product'), -('Bird Supplies', 'Product'), -('Aquarium', 'Product'), -('Small Animals', 'Product'); - -INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) -VALUES -('Premium Dog Food', 50.00, 1, 'High quality dog food'), -('Cat Toy Ball', 10.00, 2, 'Colorful toy for cats'), -('Bird Cage Large', 120.00, 3, 'Spacious bird cage'), -('Fish Tank 20 Gallon', 80.00, 4, 'Complete aquarium kit'), -('Hamster Wheel', 15.00, 5, 'Exercise wheel for small pets'), -('Organic Dog Treats', 25.00, 1, 'Natural dog treats'); - -INSERT INTO productSupplier (supId, prodId, cost) -VALUES -(1, 1, 35.00), -(1, 2, 6.50), -(2, 2, 7.00), -(3, 3, 90.00), -(3, 4, 60.00), -(4, 5, 10.00), -(5, 6, 18.00), -(1, 6, 17.50); - -INSERT INTO inventory (prodId, quantity) -VALUES -(1, 100), -(2, 200), -(3, 50), -(4, 30), -(5, 150), -(6, 75); - -INSERT INTO service (serviceName, serviceDesc, serviceDuration, servicePrice) -VALUES -('Pet Grooming', 'Full grooming service', 60, 40.00), -('Nail Trimming', 'Quick nail trim', 15, 10.00), -('Bath and Brush', 'Bathing and brushing service', 45, 30.00), -('Veterinary Checkup', 'Complete health examination', 30, 75.00), -('Teeth Cleaning', 'Professional dental cleaning', 90, 100.00); - -INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus) -VALUES -(1, 2, '2026-02-01', '10:30:00', 'Booked'), -(2, 1, '2026-02-03', '14:00:00', 'Booked'), -(3, 3, '2026-02-05', '09:00:00', 'Completed'), -(4, 4, '2026-02-07', '11:30:00', 'Booked'), -(5, 5, '2026-02-10', '15:00:00', 'Cancelled'); - -INSERT INTO appointmentPet (appointmentId, petId) -VALUES -(1, 2), -(2, 1), -(3, 3), -(4, 5), -(5, 6); - -INSERT INTO sale (saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId) -VALUES -('2026-01-05 09:15:00', 125.00, 'Card', 1, 1, 1), -('2026-01-08 11:30:00', 200.00, 'Card', 2, 1, 2), -('2026-01-12 14:20:00', 60.00, 'Cash', 3, 2, 3), -('2026-01-15 10:45:00', 150.00, 'Debit', 1, 1, 1), -('2026-01-18 16:30:00', 80.00, 'Card', 4, 3, 2), -('2026-01-22 13:15:00', 95.00, 'Cash', 2, 2, NULL), -('2026-01-25 15:40:00', 240.00, 'Card', 5, 4, 4), -('2026-01-28 10:30:00', 80.00, 'Cash', 1, 1, NULL), -('2026-02-01 09:00:00', 175.00, 'Card', 3, 3, 1), -('2026-02-03 11:20:00', 120.00, 'Card', 2, 1, 3), -('2026-02-05 14:50:00', 45.00, 'Cash', 4, 2, NULL), -('2026-02-08 16:15:00', 160.00, 'Debit', 1, 1, 2), -('2026-02-10 10:25:00', 100.00, 'Card', 5, 4, NULL), -('2026-02-12 13:45:00', 50.00, 'Cash', 2, 2, 1), -('2026-02-15 15:30:00', 85.00, 'Card', 3, 3, NULL), -('2026-02-18 11:10:00', 200.00, 'Card', 1, 1, 4), -('2026-02-20 14:35:00', 155.00, 'Debit', 4, 3, NULL), -('2026-02-22 16:50:00', 75.00, 'Cash', 2, 1, 2), -('2026-02-24 10:15:00', 140.00, 'Card', 5, 4, NULL), -(NOW(), 95.00, 'Card', 1, 1, 1); - -INSERT INTO saleItem (saleId, prodId, quantity, unitPrice) -VALUES -(1, 1, 2, 50.00), -(1, 6, 1, 25.00), -(2, 3, 1, 120.00), -(2, 4, 1, 80.00), -(3, 2, 3, 10.00), -(3, 5, 2, 15.00), -(4, 1, 3, 50.00), -(5, 4, 1, 80.00), -(6, 2, 4, 10.00), -(6, 5, 1, 15.00), -(6, 6, 1, 25.00), -(6, 1, 1, 50.00), -(7, 3, 2, 120.00), -(8, 1, 1, 50.00), -(8, 2, 3, 10.00), -(9, 1, 3, 50.00), -(9, 6, 1, 25.00), -(10, 3, 1, 120.00), -(11, 5, 1, 15.00), -(11, 2, 3, 10.00), -(12, 4, 2, 80.00), -(13, 6, 4, 25.00), -(14, 1, 1, 50.00), -(15, 2, 2, 10.00), -(15, 5, 1, 15.00), -(15, 6, 2, 25.00), -(16, 3, 1, 120.00), -(16, 4, 1, 80.00), -(17, 4, 1, 80.00), -(17, 1, 1, 50.00), -(17, 6, 1, 25.00), -(18, 6, 2, 25.00), -(18, 2, 2, 10.00), -(18, 5, 1, 15.00), -(19, 1, 2, 50.00), -(19, 6, 2, 25.00), -(20, 2, 5, 10.00), -(20, 5, 3, 15.00); - -INSERT INTO purchaseOrder (supId, orderDate, status) -VALUES -(1, '2025-01-15', 'Delivered'), -(2, '2025-01-20', 'Pending'), -(3, '2025-02-01', 'Delivered'), -(4, '2025-02-10', 'In Transit'), -(1, '2025-02-15', 'Pending'); - -INSERT INTO activityLog (employeeId, activity) -VALUES -(1, 'Created new sale'), -(2, 'Booked appointment'), -(3, 'Completed grooming service'), -(4, 'Processed inventory order'), -(5, 'Conducted health checkup'), -(1, 'Updated customer information'); diff --git a/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql b/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql deleted file mode 100644 index 2e65bc98..00000000 --- a/backend/src/main/resources/db/migration/V3__appointment_store_and_employee_store_constraints.sql +++ /dev/null @@ -1,19 +0,0 @@ -ALTER TABLE appointment - ADD COLUMN storeId BIGINT NULL AFTER customerId; - -UPDATE appointment -SET storeId = 1 -WHERE storeId IS NULL; - -ALTER TABLE appointment - MODIFY COLUMN storeId BIGINT NOT NULL, - ADD CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId); - -DELETE es1 -FROM employeeStore es1 -JOIN employeeStore es2 - ON es1.employeeId = es2.employeeId - AND es1.storeId > es2.storeId; - -ALTER TABLE employeeStore - ADD CONSTRAINT uk_employeeStore_employee UNIQUE (employeeId); diff --git a/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql b/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql deleted file mode 100644 index 271c0e72..00000000 --- a/backend/src/main/resources/db/migration/V4__conversation_mode_and_takeover.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE conversation - ADD COLUMN mode VARCHAR(20) NOT NULL DEFAULT 'AUTOMATED' AFTER status, - ADD COLUMN humanRequestedAt TIMESTAMP NULL AFTER mode; - -UPDATE conversation -SET mode = CASE - WHEN staffId IS NULL THEN 'AUTOMATED' - ELSE 'HUMAN' -END; diff --git a/backend/src/main/resources/db/migration/V5__user_token_version.sql b/backend/src/main/resources/db/migration/V5__user_token_version.sql deleted file mode 100644 index 455c4bc8..00000000 --- a/backend/src/main/resources/db/migration/V5__user_token_version.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE users - ADD COLUMN tokenVersion INT NOT NULL DEFAULT 0 AFTER active; diff --git a/backend/src/main/resources/db/migration/V6__user_phone.sql b/backend/src/main/resources/db/migration/V6__user_phone.sql deleted file mode 100644 index 95478fdc..00000000 --- a/backend/src/main/resources/db/migration/V6__user_phone.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE users - ADD COLUMN phone VARCHAR(20) NULL AFTER fullName; - -UPDATE users u -LEFT JOIN customer c ON c.user_id = u.id -LEFT JOIN employee e ON e.user_id = u.id -SET u.phone = COALESCE(NULLIF(c.phone, ''), NULLIF(e.phone, ''), u.phone) -WHERE u.phone IS NULL OR u.phone = ''; diff --git a/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql b/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql deleted file mode 100644 index fa922a82..00000000 --- a/backend/src/main/resources/db/migration/V7__employee_customer_phone_cutover.sql +++ /dev/null @@ -1,11 +0,0 @@ -UPDATE users u -LEFT JOIN customer c ON c.user_id = u.id -LEFT JOIN employee e ON e.user_id = u.id -SET u.phone = COALESCE(NULLIF(u.phone, ''), NULLIF(c.phone, ''), NULLIF(e.phone, '')) -WHERE u.phone IS NULL OR u.phone = ''; - -ALTER TABLE customer - DROP COLUMN phone; - -ALTER TABLE employee - DROP COLUMN phone; diff --git a/backend/src/main/resources/db/migration/V8__pet_product_image_urls.sql b/backend/src/main/resources/db/migration/V8__pet_product_image_urls.sql deleted file mode 100644 index a4c98248..00000000 --- a/backend/src/main/resources/db/migration/V8__pet_product_image_urls.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE pet - ADD COLUMN imageUrl VARCHAR(255) NULL; - -ALTER TABLE product - ADD COLUMN imageUrl VARCHAR(255) NULL; diff --git a/backend/src/main/resources/db/migration/V9__customer_pet.sql b/backend/src/main/resources/db/migration/V9__customer_pet.sql deleted file mode 100644 index 0981bca2..00000000 --- a/backend/src/main/resources/db/migration/V9__customer_pet.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS customer_pet ( - customer_pet_id BIGINT AUTO_INCREMENT PRIMARY KEY, - customer_id BIGINT NOT NULL, - pet_name VARCHAR(50) NOT NULL, - species VARCHAR(50) NOT NULL, - breed VARCHAR(50) NULL, - image_url VARCHAR(255) NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (customer_id) REFERENCES customer(customerId) -); diff --git a/backend/src/main/resources/dev/final-target/final_target_schema.sql b/backend/src/main/resources/dev/final-target/final_target_schema.sql new file mode 100644 index 00000000..9b8d279d --- /dev/null +++ b/backend/src/main/resources/dev/final-target/final_target_schema.sql @@ -0,0 +1,343 @@ +CREATE DATABASE IF NOT EXISTS Petstoredb_target CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE Petstoredb_target; + +CREATE TABLE IF NOT EXISTS storeLocation ( + storeId BIGINT AUTO_INCREMENT PRIMARY KEY, + storeName VARCHAR(100) NOT NULL, + address VARCHAR(255) NOT NULL, + phone VARCHAR(20) NOT NULL, + email VARCHAR(100) NOT NULL, + imageUrl VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NULL UNIQUE, + password VARCHAR(255) NULL, + email VARCHAR(100) NULL UNIQUE, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL, + fullName VARCHAR(100) NULL, + phone VARCHAR(20) NULL, + avatarUrl VARCHAR(255) NULL, + role VARCHAR(20) NOT NULL, + staffRole VARCHAR(50) NULL, + primaryStoreId BIGINT NULL, + loyaltyPoints INT NOT NULL DEFAULT 0, + active BOOLEAN NOT NULL DEFAULT TRUE, + tokenVersion INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_users_primary_store FOREIGN KEY (primaryStoreId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS supplier ( + supId BIGINT AUTO_INCREMENT PRIMARY KEY, + supCompany VARCHAR(100) NOT NULL, + supContactFirstName VARCHAR(50) NOT NULL, + supContactLastName VARCHAR(50) NOT NULL, + supEmail VARCHAR(100) NOT NULL, + supPhone VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS category ( + categoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + categoryName VARCHAR(100) NOT NULL, + categoryType VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_category_name_type UNIQUE (categoryName, categoryType) +); + +CREATE TABLE IF NOT EXISTS service ( + serviceId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceName VARCHAR(100) NOT NULL, + serviceDesc TEXT NULL, + serviceDuration INT NOT NULL, + servicePrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS service_species ( + serviceId BIGINT NOT NULL, + species VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (serviceId, species), + CONSTRAINT fk_service_species_service FOREIGN KEY (serviceId) REFERENCES service(serviceId) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS product ( + prodId BIGINT AUTO_INCREMENT PRIMARY KEY, + prodName VARCHAR(100) NOT NULL, + prodPrice DECIMAL(10, 2) NOT NULL, + categoryId BIGINT NOT NULL, + prodDesc TEXT NULL, + imageUrl VARCHAR(255) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_product_category FOREIGN KEY (categoryId) REFERENCES category(categoryId) +); + +CREATE TABLE IF NOT EXISTS inventory ( + inventoryId BIGINT AUTO_INCREMENT PRIMARY KEY, + storeId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_inventory_store_product UNIQUE (storeId, prodId), + CONSTRAINT fk_inventory_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_inventory_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS productSupplier ( + supId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + cost DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (supId, prodId), + CONSTRAINT fk_product_supplier_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), + CONSTRAINT fk_product_supplier_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS purchaseOrder ( + purchaseOrderId BIGINT AUTO_INCREMENT PRIMARY KEY, + supId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + orderDate DATE NOT NULL, + status VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_purchase_order_supplier FOREIGN KEY (supId) REFERENCES supplier(supId), + CONSTRAINT fk_purchase_order_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) +); + +CREATE TABLE IF NOT EXISTS coupon ( + couponId BIGINT AUTO_INCREMENT PRIMARY KEY, + couponCode VARCHAR(50) NOT NULL, + discountType VARCHAR(20) NOT NULL, + discountValue DECIMAL(10, 2) NOT NULL, + minOrderAmount DECIMAL(10, 2) NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, + startsAt DATETIME NULL, + endsAt DATETIME NULL, + usageLimit INT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT uq_coupon_code UNIQUE (couponCode) +); + +CREATE TABLE IF NOT EXISTS pet ( + petId BIGINT AUTO_INCREMENT PRIMARY KEY, + petName VARCHAR(50) NOT NULL, + petSpecies VARCHAR(50) NOT NULL, + petBreed VARCHAR(50) NULL, + petAge INT NULL, + petStatus VARCHAR(20) NOT NULL, + petPrice DECIMAL(10, 2) NULL, + imageUrl VARCHAR(255) NULL, + ownerUserId BIGINT NULL, + storeId BIGINT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_pet_owner_user FOREIGN KEY (ownerUserId) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_pet_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS appointment ( + appointmentId BIGINT AUTO_INCREMENT PRIMARY KEY, + serviceId BIGINT NOT NULL, + petId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + employeeId BIGINT NOT NULL, + appointmentDate DATE NOT NULL, + appointmentTime TIME NOT NULL, + appointmentStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_appointment_service FOREIGN KEY (serviceId) REFERENCES service(serviceId), + CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId), + CONSTRAINT fk_appointment_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_appointment_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_appointment_employee FOREIGN KEY (employeeId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS adoption ( + adoptionId BIGINT AUTO_INCREMENT PRIMARY KEY, + petId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + employeeId BIGINT NOT NULL, + sourceStoreId BIGINT NOT NULL, + adoptionDate DATE NOT NULL, + adoptionStatus VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_adoption_pet FOREIGN KEY (petId) REFERENCES pet(petId), + CONSTRAINT fk_adoption_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_adoption_employee FOREIGN KEY (employeeId) REFERENCES users(id), + CONSTRAINT fk_adoption_source_store FOREIGN KEY (sourceStoreId) REFERENCES storeLocation(storeId) +); + +CREATE TABLE IF NOT EXISTS cart ( + cartId BIGINT AUTO_INCREMENT PRIMARY KEY, + userId BIGINT NOT NULL, + storeId BIGINT NULL, + couponId BIGINT NULL, + cartStatus VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + subtotalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + discountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + totalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_cart_user FOREIGN KEY (userId) REFERENCES users(id), + CONSTRAINT fk_cart_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL, + CONSTRAINT fk_cart_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS cart_item ( + cartItemId BIGINT AUTO_INCREMENT PRIMARY KEY, + cartId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL, + unitPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_cart_item_cart FOREIGN KEY (cartId) REFERENCES cart(cartId) ON DELETE CASCADE, + CONSTRAINT fk_cart_item_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS sale ( + saleId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleDate DATETIME NOT NULL, + totalAmount DECIMAL(10, 2) NOT NULL, + paymentMethod VARCHAR(50) NOT NULL, + employeeId BIGINT NOT NULL, + storeId BIGINT NOT NULL, + customerId BIGINT NULL, + isRefund BOOLEAN NOT NULL DEFAULT FALSE, + originalSaleId BIGINT NULL, + channel VARCHAR(20) NOT NULL DEFAULT 'IN_STORE', + cartId BIGINT NULL, + couponId BIGINT NULL, + subtotalAmount DECIMAL(10, 2) NULL, + couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + pointsEarned INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id), + CONSTRAINT fk_sale_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), + CONSTRAINT fk_sale_customer FOREIGN KEY (customerId) REFERENCES users(id) ON DELETE SET NULL, + CONSTRAINT fk_sale_original_sale FOREIGN KEY (originalSaleId) REFERENCES sale(saleId), + CONSTRAINT fk_sale_cart FOREIGN KEY (cartId) REFERENCES cart(cartId) ON DELETE SET NULL, + CONSTRAINT fk_sale_coupon FOREIGN KEY (couponId) REFERENCES coupon(couponId) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS saleItem ( + saleItemId BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + prodId BIGINT NOT NULL, + quantity INT NOT NULL, + unitPrice DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_sale_item_sale FOREIGN KEY (saleId) REFERENCES sale(saleId) ON DELETE CASCADE, + CONSTRAINT fk_sale_item_product FOREIGN KEY (prodId) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS refund ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + saleId BIGINT NOT NULL, + customerId BIGINT NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + reason VARCHAR(500) NOT NULL, + status VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_refund_sale FOREIGN KEY (saleId) REFERENCES sale(saleId), + CONSTRAINT fk_refund_customer FOREIGN KEY (customerId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS refund_item ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + refund_id BIGINT NOT NULL, + prod_id BIGINT NOT NULL, + quantity INT NOT NULL, + unit_price DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_refund_item_refund FOREIGN KEY (refund_id) REFERENCES refund(id) ON DELETE CASCADE, + CONSTRAINT fk_refund_item_product FOREIGN KEY (prod_id) REFERENCES product(prodId) +); + +CREATE TABLE IF NOT EXISTS conversation ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + customerId BIGINT NOT NULL, + staffId BIGINT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', + mode VARCHAR(20) NOT NULL DEFAULT 'AUTOMATED', + humanRequestedAt TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_conversation_customer FOREIGN KEY (customerId) REFERENCES users(id), + CONSTRAINT fk_conversation_staff FOREIGN KEY (staffId) REFERENCES users(id) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS message ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + conversationId BIGINT NOT NULL, + senderId BIGINT NOT NULL, + content TEXT NULL, + attachmentUrl VARCHAR(255) NULL, + attachmentName VARCHAR(255) NULL, + attachmentMimeType VARCHAR(100) NULL, + attachmentSizeBytes BIGINT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + isRead BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT fk_message_conversation FOREIGN KEY (conversationId) REFERENCES conversation(id) ON DELETE CASCADE, + CONSTRAINT fk_message_sender FOREIGN KEY (senderId) REFERENCES users(id) +); + +CREATE TABLE IF NOT EXISTS activityLog ( + logId BIGINT AUTO_INCREMENT PRIMARY KEY, + userId BIGINT NOT NULL, + storeId BIGINT NULL, + activity TEXT NOT NULL, + logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id), + CONSTRAINT fk_activity_log_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId) ON DELETE SET NULL +); + +CREATE INDEX idx_users_primary_store ON users(primaryStoreId); +CREATE INDEX idx_users_role ON users(role); +CREATE INDEX idx_users_name ON users(lastName, firstName); +CREATE INDEX idx_service_species_species ON service_species(species); +CREATE INDEX idx_inventory_store ON inventory(storeId); +CREATE INDEX idx_inventory_product ON inventory(prodId); +CREATE INDEX idx_purchase_order_store ON purchaseOrder(storeId); +CREATE INDEX idx_pet_owner_user ON pet(ownerUserId); +CREATE INDEX idx_pet_store ON pet(storeId); +CREATE INDEX idx_pet_species ON pet(petSpecies); +CREATE INDEX idx_pet_name ON pet(petName); +CREATE INDEX idx_appointment_store ON appointment(storeId); +CREATE INDEX idx_appointment_employee ON appointment(employeeId); +CREATE INDEX idx_appointment_customer ON appointment(customerId); +CREATE INDEX idx_appointment_pet ON appointment(petId); +CREATE INDEX idx_appointment_date_status ON appointment(appointmentDate, appointmentStatus); +CREATE INDEX idx_adoption_store ON adoption(sourceStoreId); +CREATE INDEX idx_adoption_employee ON adoption(employeeId); +CREATE INDEX idx_sale_store ON sale(storeId); +CREATE INDEX idx_sale_employee ON sale(employeeId); +CREATE INDEX idx_sale_customer ON sale(customerId); +CREATE INDEX idx_sale_date ON sale(saleDate); +CREATE INDEX idx_cart_user ON cart(userId); +CREATE INDEX idx_conversation_customer ON conversation(customerId); +CREATE INDEX idx_conversation_staff ON conversation(staffId); +CREATE INDEX idx_activity_log_store ON activityLog(storeId); diff --git a/backend/src/main/resources/dev/final-target/final_target_seed.sql b/backend/src/main/resources/dev/final-target/final_target_seed.sql new file mode 100644 index 00000000..f1db3380 --- /dev/null +++ b/backend/src/main/resources/dev/final-target/final_target_seed.sql @@ -0,0 +1,1861 @@ +USE Petstoredb_target; + +SET FOREIGN_KEY_CHECKS = 0; + +DELETE FROM activityLog; +DELETE FROM message; +DELETE FROM conversation; +DELETE FROM refund_item; +DELETE FROM refund; +DELETE FROM saleItem; +DELETE FROM sale; +DELETE FROM cart_item; +DELETE FROM cart; +DELETE FROM adoption; +DELETE FROM appointment; +DELETE FROM pet; +DELETE FROM coupon; +DELETE FROM purchaseOrder; +DELETE FROM inventory; +DELETE FROM productSupplier; +DELETE FROM product; +DELETE FROM service_species; +DELETE FROM service; +DELETE FROM category; +DELETE FROM supplier; +DELETE FROM users; +DELETE FROM storeLocation; +ALTER TABLE storeLocation AUTO_INCREMENT = 1; +ALTER TABLE users AUTO_INCREMENT = 1; +ALTER TABLE supplier AUTO_INCREMENT = 1; +ALTER TABLE category AUTO_INCREMENT = 1; +ALTER TABLE service AUTO_INCREMENT = 1; +ALTER TABLE product AUTO_INCREMENT = 1; +ALTER TABLE inventory AUTO_INCREMENT = 1; +ALTER TABLE purchaseOrder AUTO_INCREMENT = 1; +ALTER TABLE coupon AUTO_INCREMENT = 1; +ALTER TABLE pet AUTO_INCREMENT = 1; +ALTER TABLE appointment AUTO_INCREMENT = 1; +ALTER TABLE adoption AUTO_INCREMENT = 1; +ALTER TABLE cart AUTO_INCREMENT = 1; +ALTER TABLE cart_item AUTO_INCREMENT = 1; +ALTER TABLE sale AUTO_INCREMENT = 1; +ALTER TABLE saleItem AUTO_INCREMENT = 1; +ALTER TABLE refund AUTO_INCREMENT = 1; +ALTER TABLE refund_item AUTO_INCREMENT = 1; +ALTER TABLE conversation AUTO_INCREMENT = 1; +ALTER TABLE message AUTO_INCREMENT = 1; +ALTER TABLE activityLog AUTO_INCREMENT = 1; +SET FOREIGN_KEY_CHECKS = 1; + +INSERT INTO storeLocation (storeId, storeName, address, phone, email, imageUrl) VALUES +(1, 'Downtown Branch', '123 Main St, Calgary, AB', '403-555-0101', 'downtown@petshop.com', 'https://images.petshop.local/stores/downtown.webp'), +(2, 'North Branch', '456 North Ave, Calgary, AB', '403-555-0102', 'north@petshop.com', 'https://images.petshop.local/stores/north.webp'), +(3, 'West Side Store', '789 West Blvd, Calgary, AB', '403-555-0103', 'westside@petshop.com', 'https://images.petshop.local/stores/west.webp'); + +INSERT INTO users (id, username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion) VALUES +(1, 'admin', '$2y$10$ok/BmOn/pyyamTeNmUDiB.OfLCduQlZSAaRLlupM/cZb7ZhiBriVe', 'admin@petshop.com', 'Admin', 'User', 'Admin User', '000-000-1000', 'https://images.petshop.local/users/001.webp', 'ADMIN', 'ADMINISTRATOR', 1, 0, 1, 0), +(2, 'morgan.lee', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'morgan.lee@petshop.com', 'Morgan', 'Lee', 'Morgan Lee', '403-700-0002', 'https://images.petshop.local/users/002.webp', 'ADMIN', 'OPERATIONS_ADMIN', 2, 0, 1, 0), +(3, 'staff', '$2y$10$23mqbLolo609T/.PC4KfiuY.9HqYEgA8LrJ/fccZ7CmK0/OIsPrfq', 'staff@petshop.com', 'Staff', 'User', 'Staff User', '000-000-1001', 'https://images.petshop.local/users/003.webp', 'STAFF', 'STORE_MANAGER', 1, 0, 1, 0), +(4, 'sara.smith', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sara.smith@petshop.com', 'Sara', 'Smith', 'Sara Smith', '403-710-0004', 'https://images.petshop.local/users/004.webp', 'STAFF', 'SALES_ASSOCIATE', 1, 0, 1, 0), +(5, 'david.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'david.brown@petshop.com', 'David', 'Brown', 'David Brown', '403-710-0005', 'https://images.petshop.local/users/005.webp', 'STAFF', 'VETERINARY_TECH', 1, 0, 1, 0), +(6, 'priya.patel', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'priya.patel@petshop.com', 'Priya', 'Patel', 'Priya Patel', '403-710-0006', 'https://images.petshop.local/users/006.webp', 'STAFF', 'GROOMER', 1, 0, 1, 0), +(7, 'michael.johnson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'michael.johnson@petshop.com', 'Michael', 'Johnson', 'Michael Johnson', '403-710-0007', 'https://images.petshop.local/users/007.webp', 'STAFF', 'STORE_MANAGER', 2, 0, 1, 0), +(8, 'emma.davis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'emma.davis@petshop.com', 'Emma', 'Davis', 'Emma Davis', '403-710-0008', 'https://images.petshop.local/users/008.webp', 'STAFF', 'SALES_ASSOCIATE', 2, 0, 1, 0), +(9, 'lucas.turner', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lucas.turner@petshop.com', 'Lucas', 'Turner', 'Lucas Turner', '403-710-0009', 'https://images.petshop.local/users/009.webp', 'STAFF', 'VETERINARY_TECH', 2, 0, 1, 0), +(10, 'nina.green', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nina.green@petshop.com', 'Nina', 'Green', 'Nina Green', '403-710-0010', 'https://images.petshop.local/users/010.webp', 'STAFF', 'GROOMER', 2, 0, 1, 0), +(11, 'lisa.williams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lisa.williams@petshop.com', 'Lisa', 'Williams', 'Lisa Williams', '403-710-0011', 'https://images.petshop.local/users/011.webp', 'STAFF', 'STORE_MANAGER', 3, 0, 1, 0), +(12, 'daniel.moore', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'daniel.moore@petshop.com', 'Daniel', 'Moore', 'Daniel Moore', '403-710-0012', 'https://images.petshop.local/users/012.webp', 'STAFF', 'SALES_ASSOCIATE', 3, 0, 1, 0), +(13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'VETERINARY_TECH', 3, 0, 1, 0), +(14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'GROOMER', 3, 0, 1, 0), +(15, 'customer', '$2y$10$fgIlTHDYUOzvbczwdhQP7..YuAHr2cGODb9OBQJqole3AkiY4CGUq', 'customer@petshop.com', 'Test', 'Customer', 'Test Customer', '000-000-1002', 'https://images.petshop.local/users/015.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0), +(16, 'alex.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brown@gmail.com', 'Alex', 'Brown', 'Alex Brown', '403-730-0016', 'https://images.petshop.local/users/016.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0), +(17, 'alex.clark', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.clark@gmail.com', 'Alex', 'Clark', 'Alex Clark', '403-730-0017', 'https://images.petshop.local/users/017.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), +(18, 'alex.wilson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wilson@gmail.com', 'Alex', 'Wilson', 'Alex Wilson', '403-730-0018', 'https://images.petshop.local/users/018.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(19, 'alex.martinez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.martinez@gmail.com', 'Alex', 'Martinez', 'Alex Martinez', '403-730-0019', 'https://images.petshop.local/users/019.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), +(20, 'alex.anderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.anderson@gmail.com', 'Alex', 'Anderson', 'Alex Anderson', '403-730-0020', 'https://images.petshop.local/users/020.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0), +(21, 'alex.taylor', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.taylor@gmail.com', 'Alex', 'Taylor', 'Alex Taylor', '403-730-0021', 'https://images.petshop.local/users/021.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0), +(22, 'alex.parker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.parker@gmail.com', 'Alex', 'Parker', 'Alex Parker', '403-730-0022', 'https://images.petshop.local/users/022.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0), +(23, 'alex.evans', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.evans@gmail.com', 'Alex', 'Evans', 'Alex Evans', '403-730-0023', 'https://images.petshop.local/users/023.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0), +(24, 'alex.scott', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.scott@gmail.com', 'Alex', 'Scott', 'Alex Scott', '403-730-0024', 'https://images.petshop.local/users/024.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), +(25, 'alex.adams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.adams@gmail.com', 'Alex', 'Adams', 'Alex Adams', '403-730-0025', 'https://images.petshop.local/users/025.webp', 'CUSTOMER', 'CUSTOMER', 2, 8, 1, 0), +(26, 'alex.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.baker@gmail.com', 'Alex', 'Baker', 'Alex Baker', '403-730-0026', 'https://images.petshop.local/users/026.webp', 'CUSTOMER', 'CUSTOMER', 3, 29, 1, 0), +(27, 'alex.hall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hall@gmail.com', 'Alex', 'Hall', 'Alex Hall', '403-730-0027', 'https://images.petshop.local/users/027.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0), +(28, 'alex.rivera', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.rivera@gmail.com', 'Alex', 'Rivera', 'Alex Rivera', '403-730-0028', 'https://images.petshop.local/users/028.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), +(29, 'alex.mitchell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mitchell@gmail.com', 'Alex', 'Mitchell', 'Alex Mitchell', '403-730-0029', 'https://images.petshop.local/users/029.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), +(30, 'alex.collins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.collins@gmail.com', 'Alex', 'Collins', 'Alex Collins', '403-730-0030', 'https://images.petshop.local/users/030.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0), +(31, 'alex.morris', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.morris@gmail.com', 'Alex', 'Morris', 'Alex Morris', '403-730-0031', 'https://images.petshop.local/users/031.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), +(32, 'alex.cook', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cook@gmail.com', 'Alex', 'Cook', 'Alex Cook', '403-730-0032', 'https://images.petshop.local/users/032.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), +(33, 'alex.bell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bell@gmail.com', 'Alex', 'Bell', 'Alex Bell', '403-730-0033', 'https://images.petshop.local/users/033.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(34, 'alex.reed', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reed@gmail.com', 'Alex', 'Reed', 'Alex Reed', '403-730-0034', 'https://images.petshop.local/users/034.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), +(35, 'alex.murphy', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murphy@gmail.com', 'Alex', 'Murphy', 'Alex Murphy', '403-730-0035', 'https://images.petshop.local/users/035.webp', 'CUSTOMER', 'CUSTOMER', 3, 31, 1, 0), +(36, 'alex.bailey', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bailey@gmail.com', 'Alex', 'Bailey', 'Alex Bailey', '403-730-0036', 'https://images.petshop.local/users/036.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0), +(37, 'alex.cooper', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cooper@gmail.com', 'Alex', 'Cooper', 'Alex Cooper', '403-730-0037', 'https://images.petshop.local/users/037.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0), +(38, 'alex.richardson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.richardson@gmail.com', 'Alex', 'Richardson', 'Alex Richardson', '403-730-0038', 'https://images.petshop.local/users/038.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), +(39, 'alex.cox', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cox@gmail.com', 'Alex', 'Cox', 'Alex Cox', '403-730-0039', 'https://images.petshop.local/users/039.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0), +(40, 'alex.howard', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.howard@gmail.com', 'Alex', 'Howard', 'Alex Howard', '403-730-0040', 'https://images.petshop.local/users/040.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0), +(41, 'alex.ward', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ward@gmail.com', 'Alex', 'Ward', 'Alex Ward', '403-730-0041', 'https://images.petshop.local/users/041.webp', 'CUSTOMER', 'CUSTOMER', 3, 18, 1, 0), +(42, 'alex.torres', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.torres@gmail.com', 'Alex', 'Torres', 'Alex Torres', '403-730-0042', 'https://images.petshop.local/users/042.webp', 'CUSTOMER', 'CUSTOMER', 1, 10, 1, 0), +(43, 'alex.peterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.peterson@gmail.com', 'Alex', 'Peterson', 'Alex Peterson', '403-730-0043', 'https://images.petshop.local/users/043.webp', 'CUSTOMER', 'CUSTOMER', 2, 6, 1, 0), +(44, 'alex.gray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gray@gmail.com', 'Alex', 'Gray', 'Alex Gray', '403-730-0044', 'https://images.petshop.local/users/044.webp', 'CUSTOMER', 'CUSTOMER', 3, 11, 1, 0), +(45, 'alex.ramirez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ramirez@gmail.com', 'Alex', 'Ramirez', 'Alex Ramirez', '403-730-0045', 'https://images.petshop.local/users/045.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), +(46, 'alex.james', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.james@gmail.com', 'Alex', 'James', 'Alex James', '403-730-0046', 'https://images.petshop.local/users/046.webp', 'CUSTOMER', 'CUSTOMER', 2, 28, 1, 0), +(47, 'alex.watson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.watson@gmail.com', 'Alex', 'Watson', 'Alex Watson', '403-730-0047', 'https://images.petshop.local/users/047.webp', 'CUSTOMER', 'CUSTOMER', 3, 8, 1, 0), +(48, 'alex.brooks', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brooks@gmail.com', 'Alex', 'Brooks', 'Alex Brooks', '403-730-0048', 'https://images.petshop.local/users/048.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(49, 'alex.kelly', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.kelly@gmail.com', 'Alex', 'Kelly', 'Alex Kelly', '403-730-0049', 'https://images.petshop.local/users/049.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0), +(50, 'alex.sanders', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sanders@gmail.com', 'Alex', 'Sanders', 'Alex Sanders', '403-730-0050', 'https://images.petshop.local/users/050.webp', 'CUSTOMER', 'CUSTOMER', 3, 21, 1, 0), +(51, 'alex.price', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.price@gmail.com', 'Alex', 'Price', 'Alex Price', '403-730-0051', 'https://images.petshop.local/users/051.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), +(52, 'alex.bennett', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bennett@gmail.com', 'Alex', 'Bennett', 'Alex Bennett', '403-730-0052', 'https://images.petshop.local/users/052.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), +(53, 'alex.wood', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wood@gmail.com', 'Alex', 'Wood', 'Alex Wood', '403-730-0053', 'https://images.petshop.local/users/053.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0), +(54, 'alex.barnes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.barnes@gmail.com', 'Alex', 'Barnes', 'Alex Barnes', '403-730-0054', 'https://images.petshop.local/users/054.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(55, 'alex.ross', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ross@gmail.com', 'Alex', 'Ross', 'Alex Ross', '403-730-0055', 'https://images.petshop.local/users/055.webp', 'CUSTOMER', 'CUSTOMER', 2, 7, 1, 0), +(56, 'alex.henderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.henderson@gmail.com', 'Alex', 'Henderson', 'Alex Henderson', '403-730-0056', 'https://images.petshop.local/users/056.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), +(57, 'alex.coleman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.coleman@gmail.com', 'Alex', 'Coleman', 'Alex Coleman', '403-730-0057', 'https://images.petshop.local/users/057.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(58, 'alex.jenkins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jenkins@gmail.com', 'Alex', 'Jenkins', 'Alex Jenkins', '403-730-0058', 'https://images.petshop.local/users/058.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), +(59, 'alex.perry', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.perry@gmail.com', 'Alex', 'Perry', 'Alex Perry', '403-730-0059', 'https://images.petshop.local/users/059.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), +(60, 'alex.powell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.powell@gmail.com', 'Alex', 'Powell', 'Alex Powell', '403-730-0060', 'https://images.petshop.local/users/060.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0), +(61, 'alex.long', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.long@gmail.com', 'Alex', 'Long', 'Alex Long', '403-730-0061', 'https://images.petshop.local/users/061.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), +(62, 'alex.patterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.patterson@gmail.com', 'Alex', 'Patterson', 'Alex Patterson', '403-730-0062', 'https://images.petshop.local/users/062.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0), +(63, 'alex.hughes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hughes@gmail.com', 'Alex', 'Hughes', 'Alex Hughes', '403-730-0063', 'https://images.petshop.local/users/063.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), +(64, 'alex.flores', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.flores@gmail.com', 'Alex', 'Flores', 'Alex Flores', '403-730-0064', 'https://images.petshop.local/users/064.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), +(65, 'alex.washington', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.washington@gmail.com', 'Alex', 'Washington', 'Alex Washington', '403-730-0065', 'https://images.petshop.local/users/065.webp', 'CUSTOMER', 'CUSTOMER', 3, 22, 1, 0), +(66, 'alex.butler', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.butler@gmail.com', 'Alex', 'Butler', 'Alex Butler', '403-730-0066', 'https://images.petshop.local/users/066.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), +(67, 'alex.simmons', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.simmons@gmail.com', 'Alex', 'Simmons', 'Alex Simmons', '403-730-0067', 'https://images.petshop.local/users/067.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), +(68, 'alex.foster', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.foster@gmail.com', 'Alex', 'Foster', 'Alex Foster', '403-730-0068', 'https://images.petshop.local/users/068.webp', 'CUSTOMER', 'CUSTOMER', 3, 17, 1, 0), +(69, 'alex.gonzales', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gonzales@gmail.com', 'Alex', 'Gonzales', 'Alex Gonzales', '403-730-0069', 'https://images.petshop.local/users/069.webp', 'CUSTOMER', 'CUSTOMER', 1, 15, 1, 0), +(70, 'alex.bryant', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bryant@gmail.com', 'Alex', 'Bryant', 'Alex Bryant', '403-730-0070', 'https://images.petshop.local/users/070.webp', 'CUSTOMER', 'CUSTOMER', 2, 19, 1, 0), +(71, 'alex.alexander', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.alexander@gmail.com', 'Alex', 'Alexander', 'Alex Alexander', '403-730-0071', 'https://images.petshop.local/users/071.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0), +(72, 'alex.russell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.russell@gmail.com', 'Alex', 'Russell', 'Alex Russell', '403-730-0072', 'https://images.petshop.local/users/072.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), +(73, 'alex.griffin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.griffin@gmail.com', 'Alex', 'Griffin', 'Alex Griffin', '403-730-0073', 'https://images.petshop.local/users/073.webp', 'CUSTOMER', 'CUSTOMER', 2, 2, 1, 0), +(74, 'alex.diaz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.diaz@gmail.com', 'Alex', 'Diaz', 'Alex Diaz', '403-730-0074', 'https://images.petshop.local/users/074.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0), +(75, 'alex.hayes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hayes@gmail.com', 'Alex', 'Hayes', 'Alex Hayes', '403-730-0075', 'https://images.petshop.local/users/075.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), +(76, 'alex.myers', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.myers@gmail.com', 'Alex', 'Myers', 'Alex Myers', '403-730-0076', 'https://images.petshop.local/users/076.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), +(77, 'alex.ford', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ford@gmail.com', 'Alex', 'Ford', 'Alex Ford', '403-730-0077', 'https://images.petshop.local/users/077.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0), +(78, 'alex.hamilton', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hamilton@gmail.com', 'Alex', 'Hamilton', 'Alex Hamilton', '403-730-0078', 'https://images.petshop.local/users/078.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(79, 'alex.graham', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.graham@gmail.com', 'Alex', 'Graham', 'Alex Graham', '403-730-0079', 'https://images.petshop.local/users/079.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), +(80, 'alex.sullivan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sullivan@gmail.com', 'Alex', 'Sullivan', 'Alex Sullivan', '403-730-0080', 'https://images.petshop.local/users/080.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0), +(81, 'alex.wallace', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wallace@gmail.com', 'Alex', 'Wallace', 'Alex Wallace', '403-730-0081', 'https://images.petshop.local/users/081.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0), +(82, 'alex.woods', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.woods@gmail.com', 'Alex', 'Woods', 'Alex Woods', '403-730-0082', 'https://images.petshop.local/users/082.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), +(83, 'alex.cole', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cole@gmail.com', 'Alex', 'Cole', 'Alex Cole', '403-730-0083', 'https://images.petshop.local/users/083.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0), +(84, 'alex.west', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.west@gmail.com', 'Alex', 'West', 'Alex West', '403-730-0084', 'https://images.petshop.local/users/084.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), +(85, 'alex.jordan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jordan@gmail.com', 'Alex', 'Jordan', 'Alex Jordan', '403-730-0085', 'https://images.petshop.local/users/085.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), +(86, 'alex.owens', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.owens@gmail.com', 'Alex', 'Owens', 'Alex Owens', '403-730-0086', 'https://images.petshop.local/users/086.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0), +(87, 'alex.reynolds', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reynolds@gmail.com', 'Alex', 'Reynolds', 'Alex Reynolds', '403-730-0087', 'https://images.petshop.local/users/087.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0), +(88, 'alex.fisher', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.fisher@gmail.com', 'Alex', 'Fisher', 'Alex Fisher', '403-730-0088', 'https://images.petshop.local/users/088.webp', 'CUSTOMER', 'CUSTOMER', 2, 11, 1, 0), +(89, 'alex.ellis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ellis@gmail.com', 'Alex', 'Ellis', 'Alex Ellis', '403-730-0089', 'https://images.petshop.local/users/089.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), +(90, 'alex.harrison', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.harrison@gmail.com', 'Alex', 'Harrison', 'Alex Harrison', '403-730-0090', 'https://images.petshop.local/users/090.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0), +(91, 'alex.gibson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gibson@gmail.com', 'Alex', 'Gibson', 'Alex Gibson', '403-730-0091', 'https://images.petshop.local/users/091.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), +(92, 'alex.mcdonald', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mcdonald@gmail.com', 'Alex', 'Mcdonald', 'Alex Mcdonald', '403-730-0092', 'https://images.petshop.local/users/092.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), +(93, 'alex.cruz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cruz@gmail.com', 'Alex', 'Cruz', 'Alex Cruz', '403-730-0093', 'https://images.petshop.local/users/093.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), +(94, 'alex.marshall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.marshall@gmail.com', 'Alex', 'Marshall', 'Alex Marshall', '403-730-0094', 'https://images.petshop.local/users/094.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), +(95, 'alex.ortiz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ortiz@gmail.com', 'Alex', 'Ortiz', 'Alex Ortiz', '403-730-0095', 'https://images.petshop.local/users/095.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), +(96, 'alex.gomez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gomez@gmail.com', 'Alex', 'Gomez', 'Alex Gomez', '403-730-0096', 'https://images.petshop.local/users/096.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0), +(97, 'alex.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murray@gmail.com', 'Alex', 'Murray', 'Alex Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0), +(98, 'alex.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.freeman@gmail.com', 'Alex', 'Freeman', 'Alex Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', 3, 0, 1, 0), +(99, 'alex.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wells@gmail.com', 'Alex', 'Wells', 'Alex Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0), +(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', 2, 0, 1, 0); + +INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES +(1, 'PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '403-601-1001'), +(2, 'Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '403-601-1002'), +(3, 'Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '403-601-1003'), +(4, 'Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '403-601-1004'), +(5, 'Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '403-601-1005'), +(6, 'Prairie Feeds', 'Lauren', 'Miles', 'hello@prairiefeeds.com', '403-601-1006'), +(7, 'Whisker Works', 'Darren', 'Cole', 'support@whiskerworks.com', '403-601-1007'), +(8, 'AquaLife Traders', 'Sonia', 'Bell', 'service@aqualife.com', '403-601-1008'), +(9, 'Feather & Finch', 'Maya', 'Stone', 'sales@featherfinch.com', '403-601-1009'), +(10, 'Habitat House', 'Riley', 'Ward', 'orders@habitathouse.com', '403-601-1010'), +(11, 'Trail Tails', 'Evan', 'Frost', 'contact@trailtails.com', '403-601-1011'), +(12, 'CalmPaws Health', 'Ivy', 'Brooks', 'care@calmpaws.com', '403-601-1012'); + +INSERT INTO category (categoryId, categoryName, categoryType) VALUES +(1, 'Dog Food', 'Product'), +(2, 'Cat Toys', 'Product'), +(3, 'Bird Supplies', 'Product'), +(4, 'Aquarium', 'Product'), +(5, 'Small Animals', 'Product'), +(6, 'Pet Health', 'Product'), +(7, 'Grooming Essentials', 'Product'), +(8, 'Habitats', 'Product'), +(9, 'Training & Travel', 'Product'), +(10, 'Treats & Chews', 'Product'); + +INSERT INTO service (serviceId, serviceName, serviceDesc, serviceDuration, servicePrice) VALUES +(1, 'Pet Grooming', 'Full grooming service for coat care and hygiene.', 60, 45.00), +(2, 'Nail Trimming', 'Quick nail trim for pets that need routine care.', 15, 12.00), +(3, 'Bath and Brush', 'Bathing and brushing service for shedding control.', 45, 34.00), +(4, 'Veterinary Checkup', 'General wellness check with basic health review.', 30, 80.00), +(5, 'Teeth Cleaning', 'Routine dental cleaning for eligible pets.', 50, 65.00), +(6, 'Wing Clipping', 'Safe wing trim for birds that require it.', 20, 18.00), +(7, 'Beak and Nail Care', 'Light beak and claw maintenance for birds.', 25, 22.00), +(8, 'Aquarium Health Check', 'Fish wellness and habitat consultation appointment.', 25, 28.00); + +INSERT INTO service_species (serviceId, species) VALUES +(1, 'Dog'), +(1, 'Cat'), +(1, 'Rabbit'), +(2, 'Dog'), +(2, 'Cat'), +(2, 'Rabbit'), +(2, 'Guinea Pig'), +(2, 'Hamster'), +(2, 'Bird'), +(3, 'Dog'), +(3, 'Cat'), +(3, 'Rabbit'), +(3, 'Guinea Pig'), +(4, 'Dog'), +(4, 'Cat'), +(4, 'Rabbit'), +(4, 'Bird'), +(4, 'Fish'), +(4, 'Hamster'), +(4, 'Guinea Pig'), +(5, 'Dog'), +(5, 'Cat'), +(5, 'Rabbit'), +(5, 'Guinea Pig'), +(5, 'Hamster'), +(6, 'Bird'), +(7, 'Bird'), +(8, 'Fish'); + +INSERT INTO product (prodId, prodName, prodPrice, categoryId, prodDesc, imageUrl) VALUES +(1, 'Premium Dog Food', 25.09, 1, 'Balanced nutrition for dogs. Premium Dog Food is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/001.webp'), +(2, 'Salmon Kibble Dog', 30.51, 1, 'Balanced nutrition for dogs. Salmon Kibble Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/002.webp'), +(3, 'Chicken Recipe Dog', 35.93, 1, 'Balanced nutrition for dogs. Chicken Recipe Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/003.webp'), +(4, 'Lamb Formula Dog', 41.36, 1, 'Balanced nutrition for dogs. Lamb Formula Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/004.webp'), +(5, 'Grain-Free Blend Dog', 46.78, 1, 'Balanced nutrition for dogs. Grain-Free Blend Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/005.webp'), +(6, 'Senior Dinner Dog', 52.20, 1, 'Balanced nutrition for dogs. Senior Dinner Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/006.webp'), +(7, 'Puppy Meal Dog', 57.62, 1, 'Balanced nutrition for dogs. Puppy Meal Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/007.webp'), +(8, 'Weight Control Bites Dog', 63.05, 1, 'Balanced nutrition for dogs. Weight Control Bites Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/008.webp'), +(9, 'High Energy Mix Dog', 68.47, 1, 'Balanced nutrition for dogs. High Energy Mix Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/009.webp'), +(10, 'Limited Ingredient Cuisine Dog', 73.89, 1, 'Balanced nutrition for dogs. Limited Ingredient Cuisine Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/010.webp'), +(11, 'Catnip Toy', 7.49, 2, 'Playtime toy for cats. Catnip Toy is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/011.webp'), +(12, 'Feather Chaser Cat', 9.71, 2, 'Playtime toy for cats. Feather Chaser Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/012.webp'), +(13, 'Laser Teaser Cat', 11.93, 2, 'Playtime toy for cats. Laser Teaser Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/013.webp'), +(14, 'Tunnel Ball Cat', 14.16, 2, 'Playtime toy for cats. Tunnel Ball Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/014.webp'), +(15, 'Crinkle Set Cat', 16.38, 2, 'Playtime toy for cats. Crinkle Set Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/015.webp'), +(16, 'Wand Roller Cat', 18.60, 2, 'Playtime toy for cats. Wand Roller Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/016.webp'), +(17, 'Interactive Spinner Cat', 20.82, 2, 'Playtime toy for cats. Interactive Spinner Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/017.webp'), +(18, 'Bell Track Cat', 23.05, 2, 'Playtime toy for cats. Bell Track Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/018.webp'), +(19, 'Mouse Spring Cat', 25.27, 2, 'Playtime toy for cats. Mouse Spring Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/019.webp'), +(20, 'Puzzle Bundle Cat', 27.49, 2, 'Playtime toy for cats. Puzzle Bundle Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/020.webp'), +(21, 'Perch Bird Kit', 10.89, 3, 'Everyday bird care item. Perch Bird Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/021.webp'), +(22, 'Seed Accessory Bird', 15.25, 3, 'Everyday bird care item. Seed Accessory Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/022.webp'), +(23, 'Mirror Set Bird', 19.60, 3, 'Everyday bird care item. Mirror Set Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/023.webp'), +(24, 'Ladder Pack Bird', 23.96, 3, 'Everyday bird care item. Ladder Pack Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/024.webp'), +(25, 'Bell Supply Bird', 28.31, 3, 'Everyday bird care item. Bell Supply Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/025.webp'), +(26, 'Foraging Refill Bird', 32.67, 3, 'Everyday bird care item. Foraging Refill Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/026.webp'), +(27, 'Treat Stand Bird', 37.02, 3, 'Everyday bird care item. Treat Stand Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/027.webp'), +(28, 'Cuttlebone Mix Bird', 41.38, 3, 'Everyday bird care item. Cuttlebone Mix Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/028.webp'), +(29, 'Carrier Bundle Bird', 45.73, 3, 'Everyday bird care item. Carrier Bundle Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/029.webp'), +(30, 'Bath Support Bird', 50.09, 3, 'Everyday bird care item. Bath Support Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/030.webp'), +(31, 'Nano Aquarium Kit', 21.29, 4, 'Aquarium and fish care supply. Nano Aquarium Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/031.webp'), +(32, 'Glass Accessory Aquarium', 34.00, 4, 'Aquarium and fish care supply. Glass Accessory Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/032.webp'), +(33, 'Filter Supply Aquarium', 46.71, 4, 'Aquarium and fish care supply. Filter Supply Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/033.webp'), +(34, 'Heater Filter Pack Aquarium', 59.42, 4, 'Aquarium and fish care supply. Heater Filter Pack Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/034.webp'), +(35, 'Water Tank Tool', 72.13, 4, 'Aquarium and fish care supply. Water Tank Tool is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/035.webp'), +(36, 'Coral Cleaner Aquarium', 84.85, 4, 'Aquarium and fish care supply. Coral Cleaner Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/036.webp'), +(37, 'Pebble Media Aquarium', 97.56, 4, 'Aquarium and fish care supply. Pebble Media Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/037.webp'), +(38, 'Plant Care Set Aquarium', 110.27, 4, 'Aquarium and fish care supply. Plant Care Set Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/038.webp'), +(39, 'Light Starter Aquarium', 122.98, 4, 'Aquarium and fish care supply. Light Starter Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/039.webp'), +(40, 'Pump System Aquarium', 135.69, 4, 'Aquarium and fish care supply. Pump System Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/040.webp'), +(41, 'Hay Small Pet Kit', 11.04, 5, 'Care product for rabbits, hamsters, and guinea pigs. Hay Small Pet Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/041.webp'), +(42, 'Hideout Supply Small Pet', 16.86, 5, 'Care product for rabbits, hamsters, and guinea pigs. Hideout Supply Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/042.webp'), +(43, 'Chew Care Pack Small Pet', 22.68, 5, 'Care product for rabbits, hamsters, and guinea pigs. Chew Care Pack Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/043.webp'), +(44, 'Wheel Comfort Item Small Pet', 28.51, 5, 'Care product for rabbits, hamsters, and guinea pigs. Wheel Comfort Item Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/044.webp'), +(45, 'Bottle Exercise Toy Small Pet', 34.33, 5, 'Care product for rabbits, hamsters, and guinea pigs. Bottle Exercise Toy Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/045.webp'), +(46, 'Pellet Refill Small Pet', 40.15, 5, 'Care product for rabbits, hamsters, and guinea pigs. Pellet Refill Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/046.webp'), +(47, 'Tunnel Bundle Small Pet', 45.97, 5, 'Care product for rabbits, hamsters, and guinea pigs. Tunnel Bundle Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/047.webp'), +(48, 'Bedding Snack Small Pet', 51.80, 5, 'Care product for rabbits, hamsters, and guinea pigs. Bedding Snack Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/048.webp'), +(49, 'Play Starter Small Pet', 57.62, 5, 'Care product for rabbits, hamsters, and guinea pigs. Play Starter Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/049.webp'), +(50, 'Nest Accessory Small Pet', 63.44, 5, 'Care product for rabbits, hamsters, and guinea pigs. Nest Accessory Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/050.webp'), +(51, 'Calming Support Pet', 14.09, 6, 'General health support product. Calming Support Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/051.webp'), +(52, 'Joint Drops Pet', 18.62, 6, 'General health support product. Joint Drops Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/052.webp'), +(53, 'Digestive Chew Pet', 23.16, 6, 'General health support product. Digestive Chew Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/053.webp'), +(54, 'Skin Spray Pet', 27.69, 6, 'General health support product. Skin Spray Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/054.webp'), +(55, 'Ear Kit Pet', 32.22, 6, 'General health support product. Ear Kit Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/055.webp'), +(56, 'Dental Gel Pet', 36.76, 6, 'General health support product. Dental Gel Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/056.webp'), +(57, 'Vitamin Tabs Pet', 41.29, 6, 'General health support product. Vitamin Tabs Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/057.webp'), +(58, 'Recovery Wash Pet', 45.82, 6, 'General health support product. Recovery Wash Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/058.webp'), +(59, 'Hydration Powder Pet', 50.36, 6, 'General health support product. Hydration Powder Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/059.webp'), +(60, 'Wellness Formula Pet', 54.89, 6, 'General health support product. Wellness Formula Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/060.webp'), +(61, 'Gentle Shampoo', 10.69, 7, 'Grooming essential for regular care. Gentle Shampoo is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/061.webp'), +(62, 'Deep Clean Brush', 13.09, 7, 'Grooming essential for regular care. Deep Clean Brush is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/062.webp'), +(63, 'Oatmeal Wipe', 15.49, 7, 'Grooming essential for regular care. Oatmeal Wipe is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/063.webp'), +(64, 'Deodorizing Conditioner', 17.89, 7, 'Grooming essential for regular care. Deodorizing Conditioner is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/064.webp'), +(65, 'Detangling Comb', 20.29, 7, 'Grooming essential for regular care. Detangling Comb is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/065.webp'), +(66, 'Soft Coat Mist', 22.69, 7, 'Grooming essential for regular care. Soft Coat Mist is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/066.webp'), +(67, 'Paw Foam', 25.09, 7, 'Grooming essential for regular care. Paw Foam is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/067.webp'), +(68, 'Fresh Towel', 27.49, 7, 'Grooming essential for regular care. Fresh Towel is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/068.webp'), +(69, 'Silky Rinse', 29.89, 7, 'Grooming essential for regular care. Silky Rinse is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/069.webp'), +(70, 'Quick Dry Balm', 32.29, 7, 'Grooming essential for regular care. Quick Dry Balm is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/070.webp'), +(71, 'Compact Habitat', 37.99, 8, 'Habitat or enclosure for pet comfort. Compact Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/071.webp'), +(72, 'Deluxe Crate', 53.99, 8, 'Habitat or enclosure for pet comfort. Deluxe Crate is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/072.webp'), +(73, 'Travel Carrier', 69.99, 8, 'Habitat or enclosure for pet comfort. Travel Carrier is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/073.webp'), +(74, 'Corner Enclosure', 85.99, 8, 'Habitat or enclosure for pet comfort. Corner Enclosure is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/074.webp'), +(75, 'Stacked Cage', 101.99, 8, 'Habitat or enclosure for pet comfort. Stacked Cage is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/075.webp'), +(76, 'Starter House Habitat', 117.99, 8, 'Habitat or enclosure for pet comfort. Starter House Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/076.webp'), +(77, 'Ventilated Stand Habitat', 133.99, 8, 'Habitat or enclosure for pet comfort. Ventilated Stand Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/077.webp'), +(78, 'Eco Loft Habitat', 149.99, 8, 'Habitat or enclosure for pet comfort. Eco Loft Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/078.webp'), +(79, 'Secure Playpen Habitat', 165.99, 8, 'Habitat or enclosure for pet comfort. Secure Playpen Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/079.webp'), +(80, 'Open-Air Terrarium', 181.99, 8, 'Habitat or enclosure for pet comfort. Open-Air Terrarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/080.webp'), +(81, 'Clicker Kit', 17.99, 9, 'Training and travel accessory. Clicker Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/081.webp'), +(82, 'Harness Lead', 25.10, 9, 'Training and travel accessory. Harness Lead is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/082.webp'), +(83, 'Seatbelt Set', 32.21, 9, 'Training and travel accessory. Seatbelt Set is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/083.webp'), +(84, 'Travel Bag', 39.32, 9, 'Training and travel accessory. Travel Bag is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/084.webp'), +(85, 'Training Accessory', 46.43, 9, 'Training and travel accessory. Training Accessory is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/085.webp'), +(86, 'Recall Clip', 53.55, 9, 'Training and travel accessory. Recall Clip is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/086.webp'), +(87, 'Walking Tag', 60.66, 9, 'Training and travel accessory. Walking Tag is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/087.webp'), +(88, 'Crate Pack', 67.77, 9, 'Training and travel accessory. Crate Pack is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/088.webp'), +(89, 'Carrier Mat', 74.88, 9, 'Training and travel accessory. Carrier Mat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/089.webp'), +(90, 'Adventure Guide', 81.99, 9, 'Training and travel accessory. Adventure Guide is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/090.webp'), +(91, 'Chicken Treats', 7.44, 10, 'Treat or chew for reward-based care. Chicken Treats is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/091.webp'), +(92, 'Duck Chews', 9.17, 10, 'Treat or chew for reward-based care. Duck Chews is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/092.webp'), +(93, 'Salmon Bites', 10.91, 10, 'Treat or chew for reward-based care. Salmon Bites is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/093.webp'), +(94, 'Pumpkin Snacks', 12.64, 10, 'Treat or chew for reward-based care. Pumpkin Snacks is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/094.webp'), +(95, 'Crunchy Rewards', 14.37, 10, 'Treat or chew for reward-based care. Crunchy Rewards is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/095.webp'), +(96, 'Soft-Bake Jerky', 16.11, 10, 'Treat or chew for reward-based care. Soft-Bake Jerky is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/096.webp'), +(97, 'Dental Cubes', 17.84, 10, 'Treat or chew for reward-based care. Dental Cubes is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/097.webp'), +(98, 'Mini Crisps', 19.57, 10, 'Treat or chew for reward-based care. Mini Crisps is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/098.webp'), +(99, 'Natural Morsels', 21.31, 10, 'Treat or chew for reward-based care. Natural Morsels is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/099.webp'), +(100, 'Freeze-Dried Biscuits', 23.04, 10, 'Treat or chew for reward-based care. Freeze-Dried Biscuits is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/100.webp'); + +INSERT INTO productSupplier (supId, prodId, cost) VALUES +(1, 1, 14.55), +(1, 2, 18.61), +(6, 2, 19.91), +(1, 3, 23.00), +(1, 4, 27.71), +(6, 4, 29.10), +(1, 5, 32.75), +(1, 6, 38.11), +(6, 6, 39.25), +(1, 7, 31.69), +(1, 8, 36.57), +(6, 8, 39.13), +(1, 9, 41.77), +(1, 10, 47.29), +(6, 10, 49.65), +(2, 11, 5.02), +(2, 12, 6.80), +(7, 12, 7.00), +(2, 13, 8.71), +(2, 14, 7.79), +(7, 14, 8.34), +(2, 15, 9.50), +(2, 16, 11.35), +(7, 16, 11.92), +(2, 17, 13.32), +(2, 18, 15.44), +(7, 18, 15.90), +(2, 19, 17.69), +(2, 20, 20.07), +(7, 20, 21.47), +(9, 21, 5.99), +(9, 22, 8.85), +(3, 22, 9.29), +(9, 23, 11.96), +(9, 24, 15.33), +(3, 24, 15.79), +(9, 25, 18.97), +(9, 26, 22.87), +(3, 26, 24.47), +(9, 27, 27.02), +(9, 28, 22.76), +(3, 28, 23.90), +(9, 29, 26.52), +(9, 30, 30.55), +(3, 30, 31.47), +(8, 31, 13.63), +(8, 32, 22.78), +(3, 32, 24.37), +(8, 33, 32.70), +(8, 34, 43.38), +(3, 34, 45.55), +(8, 35, 39.67), +(8, 36, 49.21), +(3, 36, 50.69), +(8, 37, 59.51), +(8, 38, 70.57), +(3, 38, 75.51), +(8, 39, 82.40), +(8, 40, 94.98), +(3, 40, 99.73), +(3, 41, 8.06), +(3, 42, 9.27), +(10, 42, 9.55), +(3, 43, 13.15), +(3, 44, 17.39), +(10, 44, 18.61), +(3, 45, 21.97), +(3, 46, 26.90), +(10, 46, 28.25), +(3, 47, 32.18), +(3, 48, 37.81), +(10, 48, 38.94), +(3, 49, 31.69), +(3, 50, 36.80), +(10, 50, 39.38), +(12, 51, 8.59), +(12, 52, 11.92), +(4, 52, 12.52), +(12, 53, 15.52), +(12, 54, 19.38), +(4, 54, 19.96), +(12, 55, 23.52), +(12, 56, 20.22), +(4, 56, 21.64), +(12, 57, 23.95), +(12, 58, 27.95), +(4, 58, 29.35), +(12, 59, 32.23), +(12, 60, 36.78), +(4, 60, 37.88), +(4, 61, 7.48), +(4, 62, 9.56), +(5, 62, 10.23), +(4, 63, 8.52), +(4, 64, 10.38), +(5, 64, 10.90), +(4, 65, 12.38), +(4, 66, 14.52), +(5, 66, 14.96), +(4, 67, 16.81), +(4, 68, 19.24), +(5, 68, 20.59), +(4, 69, 21.82), +(4, 70, 17.76), +(5, 70, 18.65), +(10, 71, 22.03), +(10, 72, 32.93), +(3, 72, 33.92), +(10, 73, 44.79), +(10, 74, 57.61), +(3, 74, 61.64), +(10, 75, 71.39), +(10, 76, 86.13), +(3, 76, 90.44), +(10, 77, 73.69), +(10, 78, 86.99), +(3, 78, 89.60), +(10, 79, 101.25), +(10, 80, 116.47), +(3, 80, 124.62), +(11, 81, 12.05), +(11, 82, 17.57), +(5, 82, 18.45), +(11, 83, 23.51), +(11, 84, 21.63), +(5, 84, 22.28), +(11, 85, 26.93), +(11, 86, 32.67), +(5, 86, 34.96), +(11, 87, 38.82), +(11, 88, 45.41), +(5, 88, 47.68), +(11, 89, 52.42), +(11, 90, 59.85), +(5, 90, 61.65), +(1, 91, 4.09), +(1, 92, 5.32), +(5, 92, 5.69), +(1, 93, 6.66), +(1, 94, 8.09), +(5, 94, 8.49), +(1, 95, 9.63), +(1, 96, 11.28), +(5, 96, 11.62), +(1, 97, 13.02), +(1, 98, 10.76), +(5, 98, 11.51), +(1, 99, 12.36), +(1, 100, 14.05), +(5, 100, 14.75); + +INSERT INTO inventory (inventoryId, storeId, prodId, quantity) VALUES +(1, 1, 1, 52), +(2, 1, 2, 59), +(3, 1, 3, 66), +(4, 1, 4, 73), +(5, 1, 5, 80), +(6, 1, 6, 87), +(7, 1, 7, 29), +(8, 1, 8, 36), +(9, 1, 9, 43), +(10, 1, 10, 50), +(11, 1, 11, 37), +(12, 1, 12, 44), +(13, 1, 13, 51), +(14, 1, 14, 58), +(15, 1, 15, 65), +(16, 1, 16, 72), +(17, 1, 17, 14), +(18, 1, 18, 21), +(19, 1, 19, 28), +(20, 1, 20, 35), +(21, 1, 21, 42), +(22, 1, 22, 49), +(23, 1, 23, 56), +(24, 1, 24, 63), +(25, 1, 25, 70), +(26, 1, 26, 12), +(27, 1, 27, 19), +(28, 1, 28, 26), +(29, 1, 29, 33), +(30, 1, 30, 40), +(31, 1, 31, 8), +(32, 1, 32, 13), +(33, 1, 33, 18), +(34, 1, 34, 5), +(35, 1, 35, 10), +(36, 1, 36, 15), +(37, 1, 37, 20), +(38, 1, 38, 7), +(39, 1, 39, 12), +(40, 1, 40, 17), +(41, 1, 41, 52), +(42, 1, 42, 59), +(43, 1, 43, 66), +(44, 1, 44, 8), +(45, 1, 45, 15), +(46, 1, 46, 22), +(47, 1, 47, 29), +(48, 1, 48, 36), +(49, 1, 49, 43), +(50, 1, 50, 50), +(51, 1, 51, 57), +(52, 1, 52, 64), +(53, 1, 53, 71), +(54, 1, 54, 13), +(55, 1, 55, 20), +(56, 1, 56, 27), +(57, 1, 57, 34), +(58, 1, 58, 41), +(59, 1, 59, 48), +(60, 1, 60, 55), +(61, 1, 61, 62), +(62, 1, 62, 69), +(63, 1, 63, 11), +(64, 1, 64, 18), +(65, 1, 65, 25), +(66, 1, 66, 32), +(67, 1, 67, 39), +(68, 1, 68, 46), +(69, 1, 69, 53), +(70, 1, 70, 60), +(71, 1, 71, 10), +(72, 1, 72, 15), +(73, 1, 73, 20), +(74, 1, 74, 7), +(75, 1, 75, 12), +(76, 1, 76, 17), +(77, 1, 77, 4), +(78, 1, 78, 9), +(79, 1, 79, 14), +(80, 1, 80, 19), +(81, 1, 81, 72), +(82, 1, 82, 14), +(83, 1, 83, 21), +(84, 1, 84, 28), +(85, 1, 85, 35), +(86, 1, 86, 42), +(87, 1, 87, 49), +(88, 1, 88, 56), +(89, 1, 89, 63), +(90, 1, 90, 70), +(91, 1, 91, 32), +(92, 1, 92, 39), +(93, 1, 93, 46), +(94, 1, 94, 53), +(95, 1, 95, 60), +(96, 1, 96, 67), +(97, 1, 97, 74), +(98, 1, 98, 81), +(99, 1, 99, 88), +(100, 1, 100, 30), +(101, 2, 1, 69), +(102, 2, 2, 76), +(103, 2, 3, 83), +(104, 2, 4, 90), +(105, 2, 5, 32), +(106, 2, 6, 39), +(107, 2, 7, 46), +(108, 2, 8, 53), +(109, 2, 9, 60), +(110, 2, 10, 67), +(111, 2, 11, 54), +(112, 2, 12, 61), +(113, 2, 13, 68), +(114, 2, 14, 10), +(115, 2, 15, 17), +(116, 2, 16, 24), +(117, 2, 17, 31), +(118, 2, 18, 38), +(119, 2, 19, 45), +(120, 2, 20, 52), +(121, 2, 21, 59), +(122, 2, 22, 66), +(123, 2, 23, 8), +(124, 2, 24, 15), +(125, 2, 25, 22), +(126, 2, 26, 29), +(127, 2, 27, 36), +(128, 2, 28, 43), +(129, 2, 29, 50), +(130, 2, 30, 57), +(131, 2, 31, 19), +(132, 2, 32, 6), +(133, 2, 33, 11), +(134, 2, 34, 16), +(135, 2, 35, 21), +(136, 2, 36, 8), +(137, 2, 37, 13), +(138, 2, 38, 18), +(139, 2, 39, 5), +(140, 2, 40, 10), +(141, 2, 41, 69), +(142, 2, 42, 11), +(143, 2, 43, 18), +(144, 2, 44, 25), +(145, 2, 45, 32), +(146, 2, 46, 39), +(147, 2, 47, 46), +(148, 2, 48, 53), +(149, 2, 49, 60), +(150, 2, 50, 67), +(151, 2, 51, 9), +(152, 2, 52, 16), +(153, 2, 53, 23), +(154, 2, 54, 30), +(155, 2, 55, 37), +(156, 2, 56, 44), +(157, 2, 57, 51), +(158, 2, 58, 58), +(159, 2, 59, 65), +(160, 2, 60, 72), +(161, 2, 61, 14), +(162, 2, 62, 21), +(163, 2, 63, 28), +(164, 2, 64, 35), +(165, 2, 65, 42), +(166, 2, 66, 49), +(167, 2, 67, 56), +(168, 2, 68, 63), +(169, 2, 69, 70), +(170, 2, 70, 12), +(171, 2, 71, 21), +(172, 2, 72, 8), +(173, 2, 73, 13), +(174, 2, 74, 18), +(175, 2, 75, 5), +(176, 2, 76, 10), +(177, 2, 77, 15), +(178, 2, 78, 20), +(179, 2, 79, 7), +(180, 2, 80, 12), +(181, 2, 81, 24), +(182, 2, 82, 31), +(183, 2, 83, 38), +(184, 2, 84, 45), +(185, 2, 85, 52), +(186, 2, 86, 59), +(187, 2, 87, 66), +(188, 2, 88, 8), +(189, 2, 89, 15), +(190, 2, 90, 22), +(191, 2, 91, 49), +(192, 2, 92, 56), +(193, 2, 93, 63), +(194, 2, 94, 70), +(195, 2, 95, 77), +(196, 2, 96, 84), +(197, 2, 97, 91), +(198, 2, 98, 33), +(199, 2, 99, 40), +(200, 2, 100, 47), +(201, 3, 1, 86), +(202, 3, 2, 28), +(203, 3, 3, 35), +(204, 3, 4, 42), +(205, 3, 5, 49), +(206, 3, 6, 56), +(207, 3, 7, 63), +(208, 3, 8, 70), +(209, 3, 9, 77), +(210, 3, 10, 84), +(211, 3, 11, 71), +(212, 3, 12, 13), +(213, 3, 13, 20), +(214, 3, 14, 27), +(215, 3, 15, 34), +(216, 3, 16, 41), +(217, 3, 17, 48), +(218, 3, 18, 55), +(219, 3, 19, 62), +(220, 3, 20, 69), +(221, 3, 21, 11), +(222, 3, 22, 18), +(223, 3, 23, 25), +(224, 3, 24, 32), +(225, 3, 25, 39), +(226, 3, 26, 46), +(227, 3, 27, 53), +(228, 3, 28, 60), +(229, 3, 29, 67), +(230, 3, 30, 9), +(231, 3, 31, 12), +(232, 3, 32, 17), +(233, 3, 33, 4), +(234, 3, 34, 9), +(235, 3, 35, 14), +(236, 3, 36, 19), +(237, 3, 37, 6), +(238, 3, 38, 11), +(239, 3, 39, 16), +(240, 3, 40, 21), +(241, 3, 41, 21), +(242, 3, 42, 28), +(243, 3, 43, 35), +(244, 3, 44, 42), +(245, 3, 45, 49), +(246, 3, 46, 56), +(247, 3, 47, 63), +(248, 3, 48, 70), +(249, 3, 49, 12), +(250, 3, 50, 19), +(251, 3, 51, 26), +(252, 3, 52, 33), +(253, 3, 53, 40), +(254, 3, 54, 47), +(255, 3, 55, 54), +(256, 3, 56, 61), +(257, 3, 57, 68), +(258, 3, 58, 10), +(259, 3, 59, 17), +(260, 3, 60, 24), +(261, 3, 61, 31), +(262, 3, 62, 38), +(263, 3, 63, 45), +(264, 3, 64, 52), +(265, 3, 65, 59), +(266, 3, 66, 66), +(267, 3, 67, 8), +(268, 3, 68, 15), +(269, 3, 69, 22), +(270, 3, 70, 29), +(271, 3, 71, 14), +(272, 3, 72, 19), +(273, 3, 73, 6), +(274, 3, 74, 11), +(275, 3, 75, 16), +(276, 3, 76, 21), +(277, 3, 77, 8), +(278, 3, 78, 13), +(279, 3, 79, 18), +(280, 3, 80, 5), +(281, 3, 81, 41), +(282, 3, 82, 48), +(283, 3, 83, 55), +(284, 3, 84, 62), +(285, 3, 85, 69), +(286, 3, 86, 11), +(287, 3, 87, 18), +(288, 3, 88, 25), +(289, 3, 89, 32), +(290, 3, 90, 39), +(291, 3, 91, 66), +(292, 3, 92, 73), +(293, 3, 93, 80), +(294, 3, 94, 87), +(295, 3, 95, 29), +(296, 3, 96, 36), +(297, 3, 97, 43), +(298, 3, 98, 50), +(299, 3, 99, 57), +(300, 3, 100, 64); + +INSERT INTO purchaseOrder (purchaseOrderId, supId, storeId, orderDate, status) VALUES +(1, 3, 1, '2026-01-06', 'RECEIVED'), +(2, 4, 1, '2026-01-13', 'RECEIVED'), +(3, 5, 1, '2026-01-20', 'RECEIVED'), +(4, 4, 2, '2026-01-07', 'RECEIVED'), +(5, 5, 2, '2026-01-14', 'RECEIVED'), +(6, 6, 2, '2026-01-21', 'RECEIVED'), +(7, 5, 3, '2026-01-08', 'RECEIVED'), +(8, 6, 3, '2026-01-15', 'RECEIVED'), +(9, 7, 3, '2026-01-22', 'RECEIVED'), +(10, 4, 1, '2026-02-06', 'RECEIVED'), +(11, 5, 1, '2026-02-13', 'RECEIVED'), +(12, 6, 1, '2026-02-20', 'RECEIVED'), +(13, 5, 2, '2026-02-07', 'RECEIVED'), +(14, 6, 2, '2026-02-14', 'RECEIVED'), +(15, 7, 2, '2026-02-21', 'RECEIVED'), +(16, 6, 3, '2026-02-08', 'RECEIVED'), +(17, 7, 3, '2026-02-15', 'RECEIVED'), +(18, 8, 3, '2026-02-22', 'RECEIVED'), +(19, 5, 1, '2026-03-06', 'RECEIVED'), +(20, 6, 1, '2026-03-13', 'RECEIVED'), +(21, 7, 1, '2026-03-20', 'RECEIVED'), +(22, 6, 2, '2026-03-07', 'RECEIVED'), +(23, 7, 2, '2026-03-14', 'RECEIVED'), +(24, 8, 2, '2026-03-21', 'RECEIVED'), +(25, 7, 3, '2026-03-08', 'RECEIVED'), +(26, 8, 3, '2026-03-15', 'RECEIVED'), +(27, 9, 3, '2026-03-22', 'RECEIVED'), +(28, 6, 1, '2026-04-06', 'PENDING'), +(29, 7, 1, '2026-04-13', 'RECEIVED'), +(30, 8, 1, '2026-04-20', 'PLACED'), +(31, 7, 2, '2026-04-07', 'RECEIVED'), +(32, 8, 2, '2026-04-14', 'PLACED'), +(33, 9, 2, '2026-04-21', 'PENDING'), +(34, 8, 3, '2026-04-08', 'PLACED'), +(35, 9, 3, '2026-04-15', 'PENDING'), +(36, 10, 3, '2026-04-22', 'RECEIVED'); + +INSERT INTO coupon (couponId, couponCode, discountType, discountValue, minOrderAmount, active, startsAt, endsAt, usageLimit) VALUES +(1, 'NOCODE', 'FIXED', 0.00, 0.00, 1, NULL, NULL, NULL), +(2, 'WELCOME10', 'PERCENT', 10.00, 50.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 300), +(3, 'TREAT5', 'FIXED', 5.00, 25.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 500), +(4, 'GROOM15', 'PERCENT', 15.00, 60.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 200), +(5, 'FISHCARE8', 'FIXED', 8.00, 40.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(6, 'BIRD10', 'PERCENT', 10.00, 30.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(7, 'SPRING12', 'PERCENT', 12.00, 75.00, 1, '2026-03-01 00:00:00', '2026-05-31 23:59:59', 180), +(8, 'NEWPET20', 'FIXED', 20.00, 100.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 120); + +INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, ownerUserId, storeId) VALUES +(1, 'Buddy', 'Dog', 'Corgi', 2, 'Available', 466.80, 'https://images.petshop.local/pets/001.webp', NULL, 1), +(2, 'Milo', 'Dog', 'Beagle', 3, 'Available', 513.60, 'https://images.petshop.local/pets/002.webp', NULL, 1), +(3, 'Charlie', 'Dog', 'Husky', 4, 'Available', 560.40, 'https://images.petshop.local/pets/003.webp', NULL, 1), +(4, 'Luna', 'Cat', 'Persian', 5, 'Available', 395.20, 'https://images.petshop.local/pets/004.webp', NULL, 1), +(5, 'Max', 'Cat', 'Siamese', 6, 'Available', 429.00, 'https://images.petshop.local/pets/005.webp', NULL, 1), +(6, 'Bella', 'Rabbit', 'Dutch', 1, 'Available', 211.40, 'https://images.petshop.local/pets/006.webp', NULL, 1), +(7, 'Rocky', 'Bird', 'Budgie', 2, 'Available', 169.20, 'https://images.petshop.local/pets/007.webp', NULL, 1), +(8, 'Daisy', 'Fish', 'Guppy', 3, 'Available', 16.20, 'https://images.petshop.local/pets/008.webp', NULL, 1), +(9, 'Cooper', 'Hamster', 'Roborovski', 4, 'Available', 30.10, 'https://images.petshop.local/pets/009.webp', NULL, 1), +(10, 'Ruby', 'Guinea Pig', 'Abyssinian', 5, 'Available', 53.50, 'https://images.petshop.local/pets/010.webp', NULL, 1), +(11, 'Tucker', 'Dog', 'Border Collie', 6, 'Available', 574.80, 'https://images.petshop.local/pets/011.webp', NULL, 1), +(12, 'Rosie', 'Cat', 'Maine Coon', 1, 'Available', 405.60, 'https://images.petshop.local/pets/012.webp', NULL, 1), +(13, 'Bear', 'Dog', 'Labrador', 2, 'Available', 668.40, 'https://images.petshop.local/pets/013.webp', NULL, 2), +(14, 'Maggie', 'Dog', 'Golden Retriever', 3, 'Available', 715.20, 'https://images.petshop.local/pets/014.webp', NULL, 2), +(15, 'Leo', 'Dog', 'Husky', 4, 'Available', 762.00, 'https://images.petshop.local/pets/015.webp', NULL, 2), +(16, 'Zoey', 'Cat', 'Scottish Fold', 5, 'Available', 280.80, 'https://images.petshop.local/pets/016.webp', NULL, 2), +(17, 'Oliver', 'Cat', 'Siamese', 6, 'Available', 314.60, 'https://images.petshop.local/pets/017.webp', NULL, 2), +(18, 'Lola', 'Rabbit', 'Netherland Dwarf', 1, 'Available', 154.20, 'https://images.petshop.local/pets/018.webp', NULL, 2), +(19, 'Buster', 'Bird', 'Budgie', 2, 'Available', 116.40, 'https://images.petshop.local/pets/019.webp', NULL, 2), +(20, 'Sadie', 'Fish', 'Tetra', 3, 'Available', 33.00, 'https://images.petshop.local/pets/020.webp', NULL, 2), +(21, 'Toby', 'Hamster', 'Dwarf', 4, 'Available', 46.90, 'https://images.petshop.local/pets/021.webp', NULL, 2), +(22, 'Cleo', 'Guinea Pig', 'Abyssinian', 5, 'Available', 78.70, 'https://images.petshop.local/pets/022.webp', NULL, 2), +(23, 'Harley', 'Dog', 'Boxer', 6, 'Available', 776.40, 'https://images.petshop.local/pets/023.webp', NULL, 2), +(24, 'Mocha', 'Cat', 'Siamese', 1, 'Available', 291.20, 'https://images.petshop.local/pets/024.webp', NULL, 2), +(25, 'Rex', 'Dog', 'Poodle', 2, 'Available', 510.00, 'https://images.petshop.local/pets/025.webp', NULL, 3), +(26, 'Willow', 'Dog', 'Boxer', 3, 'Available', 556.80, 'https://images.petshop.local/pets/026.webp', NULL, 3), +(27, 'Gizmo', 'Dog', 'Labrador', 4, 'Available', 603.60, 'https://images.petshop.local/pets/027.webp', NULL, 3), +(28, 'Nala', 'Cat', 'Calico', 5, 'Available', 426.40, 'https://images.petshop.local/pets/028.webp', NULL, 3), +(29, 'Duke', 'Cat', 'Calico', 6, 'Available', 460.20, 'https://images.petshop.local/pets/029.webp', NULL, 3), +(30, 'Misty', 'Rabbit', 'Lionhead', 1, 'Available', 227.00, 'https://images.petshop.local/pets/030.webp', NULL, 3), +(31, 'Ace', 'Bird', 'Budgie', 2, 'Available', 63.60, 'https://images.petshop.local/pets/031.webp', NULL, 3), +(32, 'Pepper', 'Fish', 'Goldfish', 3, 'Available', 19.80, 'https://images.petshop.local/pets/032.webp', NULL, 3), +(33, 'Coco', 'Hamster', 'Syrian', 4, 'Available', 33.70, 'https://images.petshop.local/pets/033.webp', NULL, 3), +(34, 'Finn', 'Guinea Pig', 'Peruvian', 5, 'Available', 58.90, 'https://images.petshop.local/pets/034.webp', NULL, 3), +(35, 'Shadow', 'Dog', 'Beagle', 6, 'Available', 618.00, 'https://images.petshop.local/pets/035.webp', NULL, 3), +(36, 'Kitty', 'Cat', 'British Shorthair', 1, 'Available', 436.80, 'https://images.petshop.local/pets/036.webp', NULL, 3), +(37, 'Bruno', 'Bird', 'Cockatiel', 6, 'Adopted', 94.80, 'https://images.petshop.local/pets/037.webp', 16, 1), +(38, 'Snowball', 'Fish', 'Betta', 8, 'Adopted', 28.80, 'https://images.petshop.local/pets/038.webp', 17, 2), +(39, 'Zeus', 'Fish', 'Guppy', 2, 'Adopted', 33.90, 'https://images.petshop.local/pets/039.webp', 18, 3), +(40, 'Biscuit', 'Fish', 'Goldfish', 4, 'Adopted', 39.00, 'https://images.petshop.local/pets/040.webp', 19, 1), +(41, 'Patches', 'Dog', 'Boxer', 6, 'Adopted', 769.20, 'https://images.petshop.local/pets/041.webp', 20, 2), +(42, 'Scout', 'Fish', 'Goldfish', 8, 'Adopted', 19.20, 'https://images.petshop.local/pets/042.webp', 21, 3), +(43, 'Mittens', 'Rabbit', 'Holland Lop', 2, 'Adopted', 150.30, 'https://images.petshop.local/pets/043.webp', 22, 1), +(44, 'Thor', 'Fish', 'Betta', 4, 'Adopted', 29.40, 'https://images.petshop.local/pets/044.webp', 23, 2), +(45, 'Whiskers', 'Fish', 'Betta', 6, 'Adopted', 34.50, 'https://images.petshop.local/pets/045.webp', 24, 3), +(46, 'Goldie', 'Fish', 'Goldfish', 8, 'Adopted', 39.60, 'https://images.petshop.local/pets/046.webp', 25, 1), +(47, 'Midnight', 'Bird', 'Parakeet', 2, 'Adopted', 178.80, 'https://images.petshop.local/pets/047.webp', 26, 2), +(48, 'Storm', 'Bird', 'Canary', 4, 'Adopted', 79.20, 'https://images.petshop.local/pets/048.webp', 27, 3), +(49, 'Peanut', 'Bird', 'Parakeet', 6, 'Adopted', 99.60, 'https://images.petshop.local/pets/049.webp', 28, 1), +(50, 'Daisy', 'Bird', 'Canary', 8, 'Adopted', 120.00, 'https://images.petshop.local/pets/050.webp', 29, 2), +(51, 'Cleo', 'Rabbit', 'Netherland Dwarf', 2, 'Adopted', 197.10, 'https://images.petshop.local/pets/051.webp', 30, 3), +(52, 'Sunny', 'Cat', 'Maine Coon', 4, 'Adopted', 478.40, 'https://images.petshop.local/pets/052.webp', 31, 1), +(53, 'Maple', 'Dog', 'Boxer', 6, 'Adopted', 423.60, 'https://images.petshop.local/pets/053.webp', 32, 2), +(54, 'Nova', 'Rabbit', 'Dutch', 8, 'Adopted', 133.40, 'https://images.petshop.local/pets/054.webp', 33, 3), +(55, 'Piper', 'Dog', 'Shih Tzu', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/055.webp', 34, 2), +(56, 'Hazel', 'Cat', 'Bengal', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/056.webp', 35, 3), +(57, 'Jasper', 'Rabbit', 'Lionhead', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/057.webp', 36, 1), +(58, 'Remy', 'Bird', 'Canary', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/058.webp', 37, 2), +(59, 'Archie', 'Fish', 'Tetra', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/059.webp', 38, 3), +(60, 'Skye', 'Hamster', 'Syrian', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/060.webp', 39, 1), +(61, 'Otis', 'Guinea Pig', 'Abyssinian', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/061.webp', 40, 2), +(62, 'Marley', 'Dog', 'Border Collie', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/062.webp', 41, 3), +(63, 'Blue', 'Cat', 'Scottish Fold', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/063.webp', 42, 1), +(64, 'Honey', 'Rabbit', 'Netherland Dwarf', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/064.webp', 43, 2), +(65, 'Mochi', 'Bird', 'Canary', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/065.webp', 44, 3), +(66, 'Kiki', 'Fish', 'Goldfish', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/066.webp', 45, 1), +(67, 'River', 'Hamster', 'Dwarf', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/067.webp', 46, 2), +(68, 'Bowie', 'Guinea Pig', 'American', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/068.webp', 47, 3), +(69, 'Sage', 'Dog', 'Labrador', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/069.webp', 48, 1), +(70, 'Echo', 'Cat', 'Siamese', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/070.webp', 49, 2), +(71, 'Poppy', 'Rabbit', 'Dutch', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/071.webp', 50, 3), +(72, 'Juniper', 'Bird', 'Parakeet', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/072.webp', 51, 1), +(73, 'Winston', 'Fish', 'Guppy', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/073.webp', 52, 2), +(74, 'Freya', 'Hamster', 'Dwarf', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/074.webp', 53, 3), +(75, 'Finnley', 'Guinea Pig', 'Peruvian', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/075.webp', 54, 1), +(76, 'Louie', 'Dog', 'Corgi', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/076.webp', 55, 2), +(77, 'Ivy', 'Cat', 'Calico', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/077.webp', 56, 3), +(78, 'Binx', 'Rabbit', 'Lionhead', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/078.webp', 57, 1), +(79, 'Suki', 'Bird', 'Parakeet', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/079.webp', 58, 2), +(80, 'Mabel', 'Fish', 'Molly', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/080.webp', 59, 3), +(81, 'Rolo', 'Hamster', 'Syrian', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/081.webp', 60, 1), +(82, 'Clover', 'Guinea Pig', 'Abyssinian', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/082.webp', 61, 2), +(83, 'Frankie', 'Dog', 'German Shepherd', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/083.webp', 62, 3), +(84, 'Tilly', 'Cat', 'Tabby', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/084.webp', 63, 1), +(85, 'Rory', 'Rabbit', 'Holland Lop', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/085.webp', 64, 2), +(86, 'Gus', 'Bird', 'Budgie', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/086.webp', 65, 3), +(87, 'Peaches', 'Fish', 'Guppy', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/087.webp', 66, 1), +(88, 'Indie', 'Hamster', 'Roborovski', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/088.webp', 67, 2), +(89, 'Minnie', 'Guinea Pig', 'Peruvian', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/089.webp', 68, 3), +(90, 'Koda', 'Dog', 'Shih Tzu', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/090.webp', 69, 1), +(91, 'Mango', 'Cat', 'British Shorthair', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/091.webp', 70, 2), +(92, 'Pearl', 'Rabbit', 'Lionhead', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/092.webp', 71, 3), +(93, 'Onyx', 'Bird', 'Canary', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/093.webp', 72, 1), +(94, 'Pumpkin', 'Fish', 'Betta', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/094.webp', 73, 2), +(95, 'Nori', 'Hamster', 'Dwarf', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/095.webp', 74, 3), +(96, 'Cosmo', 'Guinea Pig', 'American', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/096.webp', 75, 1), +(97, 'Ziggy', 'Dog', 'Beagle', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/097.webp', 76, 2), +(98, 'Bean', 'Cat', 'Calico', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/098.webp', 77, 3), +(99, 'Flora', 'Rabbit', 'Holland Lop', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/099.webp', 78, 1), +(100, 'Comet', 'Bird', 'Lovebird', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/100.webp', 79, 2); + +INSERT INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES +(1, 2, 37, 16, 1, 3, '2026-01-07', '09:00:00', 'COMPLETED'), +(2, 8, 38, 17, 2, 8, '2026-01-09', '10:30:00', 'COMPLETED'), +(3, 4, 39, 18, 3, 13, '2026-01-11', '13:00:00', 'MISSED'), +(4, 8, 40, 19, 1, 6, '2026-01-13', '14:30:00', 'CANCELLED'), +(5, 5, 41, 20, 2, 7, '2026-01-15', '16:00:00', 'COMPLETED'), +(6, 8, 42, 21, 3, 12, '2026-01-17', '09:00:00', 'COMPLETED'), +(7, 2, 43, 22, 1, 5, '2026-01-19', '10:30:00', 'COMPLETED'), +(8, 8, 44, 23, 2, 10, '2026-01-21', '13:00:00', 'MISSED'), +(9, 4, 45, 24, 3, 11, '2026-01-23', '14:30:00', 'CANCELLED'), +(10, 8, 46, 25, 1, 4, '2026-01-25', '16:00:00', 'COMPLETED'), +(11, 6, 47, 26, 2, 9, '2026-01-27', '09:00:00', 'COMPLETED'), +(12, 7, 48, 27, 3, 14, '2026-01-29', '10:30:00', 'COMPLETED'), +(13, 2, 49, 28, 1, 3, '2026-01-31', '13:00:00', 'MISSED'), +(14, 4, 50, 29, 2, 8, '2026-02-02', '14:30:00', 'CANCELLED'), +(15, 5, 51, 30, 3, 13, '2026-02-04', '16:00:00', 'COMPLETED'), +(16, 1, 52, 31, 1, 6, '2026-02-06', '09:00:00', 'COMPLETED'), +(17, 2, 53, 32, 2, 7, '2026-02-08', '10:30:00', 'COMPLETED'), +(18, 3, 54, 33, 3, 12, '2026-02-10', '13:00:00', 'MISSED'), +(19, 4, 55, 34, 2, 9, '2026-02-12', '14:30:00', 'CANCELLED'), +(20, 5, 56, 35, 3, 14, '2026-02-14', '16:00:00', 'COMPLETED'), +(21, 1, 57, 36, 1, 3, '2026-02-16', '09:00:00', 'COMPLETED'), +(22, 4, 58, 37, 2, 8, '2026-02-18', '10:30:00', 'COMPLETED'), +(23, 4, 59, 38, 3, 13, '2026-02-20', '13:00:00', 'MISSED'), +(24, 5, 60, 39, 1, 6, '2026-02-22', '14:30:00', 'CANCELLED'), +(25, 2, 61, 40, 2, 7, '2026-02-24', '16:00:00', 'COMPLETED'), +(26, 1, 62, 41, 3, 12, '2026-02-26', '09:00:00', 'COMPLETED'), +(27, 2, 63, 42, 1, 5, '2026-02-28', '10:30:00', 'COMPLETED'), +(28, 3, 64, 43, 2, 10, '2026-03-02', '13:00:00', 'MISSED'), +(29, 2, 65, 44, 3, 11, '2026-03-04', '14:30:00', 'CANCELLED'), +(30, 8, 66, 45, 1, 4, '2026-03-06', '16:00:00', 'COMPLETED'), +(31, 2, 67, 46, 2, 9, '2026-03-08', '09:00:00', 'COMPLETED'), +(32, 5, 68, 47, 3, 14, '2026-03-10', '10:30:00', 'COMPLETED'), +(33, 3, 69, 48, 1, 3, '2026-03-12', '13:00:00', 'MISSED'), +(34, 4, 70, 49, 2, 8, '2026-03-14', '14:30:00', 'CANCELLED'), +(35, 5, 71, 50, 3, 13, '2026-03-16', '16:00:00', 'COMPLETED'), +(36, 7, 72, 51, 1, 6, '2026-03-18', '09:00:00', 'COMPLETED'), +(37, 4, 73, 52, 2, 7, '2026-03-20', '10:30:00', 'COMPLETED'), +(38, 4, 74, 53, 3, 12, '2026-03-22', '13:00:00', 'MISSED'), +(39, 4, 75, 54, 1, 5, '2026-03-24', '14:30:00', 'CANCELLED'), +(40, 5, 76, 55, 2, 10, '2026-03-26', '16:00:00', 'COMPLETED'), +(41, 1, 77, 56, 3, 11, '2026-03-28', '09:00:00', 'COMPLETED'), +(42, 2, 78, 57, 1, 4, '2026-03-30', '10:30:00', 'COMPLETED'), +(43, 6, 79, 58, 2, 9, '2026-04-01', '13:00:00', 'BOOKED'), +(44, 8, 80, 59, 3, 14, '2026-04-03', '14:30:00', 'BOOKED'), +(45, 5, 81, 60, 1, 3, '2026-04-05', '16:00:00', 'BOOKED'), +(46, 3, 82, 61, 2, 8, '2026-04-07', '09:00:00', 'BOOKED'), +(47, 2, 83, 62, 3, 13, '2026-04-09', '10:30:00', 'BOOKED'), +(48, 3, 84, 63, 1, 6, '2026-04-11', '13:00:00', 'BOOKED'), +(49, 4, 85, 64, 2, 7, '2026-04-13', '14:30:00', 'BOOKED'), +(50, 4, 86, 65, 3, 12, '2026-04-15', '16:00:00', 'BOOKED'), +(51, 4, 87, 66, 1, 5, '2026-04-17', '09:00:00', 'BOOKED'), +(52, 2, 88, 67, 2, 10, '2026-04-19', '10:30:00', 'BOOKED'), +(53, 2, 89, 68, 3, 11, '2026-04-21', '13:00:00', 'BOOKED'), +(54, 4, 90, 69, 1, 4, '2026-04-23', '14:30:00', 'BOOKED'), +(55, 5, 91, 70, 2, 9, '2026-04-25', '16:00:00', 'BOOKED'), +(56, 1, 92, 71, 3, 14, '2026-04-27', '09:00:00', 'BOOKED'), +(57, 2, 93, 72, 1, 3, '2026-04-29', '10:30:00', 'BOOKED'), +(58, 8, 94, 73, 2, 8, '2026-05-01', '13:00:00', 'BOOKED'), +(59, 4, 95, 74, 3, 13, '2026-05-03', '14:30:00', 'BOOKED'), +(60, 5, 96, 75, 1, 6, '2026-05-05', '16:00:00', 'BOOKED'), +(61, 1, 97, 76, 2, 7, '2026-01-07', '09:00:00', 'COMPLETED'), +(62, 2, 98, 77, 3, 12, '2026-01-09', '10:30:00', 'COMPLETED'), +(63, 3, 99, 78, 1, 5, '2026-01-11', '13:00:00', 'MISSED'), +(64, 7, 100, 79, 2, 10, '2026-01-13', '14:30:00', 'CANCELLED'), +(65, 2, 37, 16, 1, 3, '2026-01-15', '16:00:00', 'COMPLETED'), +(66, 8, 38, 17, 2, 8, '2026-01-17', '09:00:00', 'COMPLETED'), +(67, 4, 39, 18, 3, 13, '2026-01-19', '10:30:00', 'COMPLETED'), +(68, 8, 40, 19, 1, 6, '2026-01-21', '13:00:00', 'MISSED'), +(69, 4, 41, 20, 2, 7, '2026-01-23', '14:30:00', 'CANCELLED'), +(70, 8, 42, 21, 3, 12, '2026-01-25', '16:00:00', 'COMPLETED'), +(71, 1, 43, 22, 1, 5, '2026-01-27', '09:00:00', 'COMPLETED'), +(72, 8, 44, 23, 2, 10, '2026-01-29', '10:30:00', 'COMPLETED'), +(73, 4, 45, 24, 3, 11, '2026-01-31', '13:00:00', 'MISSED'), +(74, 8, 46, 25, 1, 4, '2026-02-02', '14:30:00', 'CANCELLED'), +(75, 6, 47, 26, 2, 9, '2026-02-04', '16:00:00', 'COMPLETED'), +(76, 7, 48, 27, 3, 14, '2026-02-06', '09:00:00', 'COMPLETED'), +(77, 2, 49, 28, 1, 3, '2026-02-08', '10:30:00', 'COMPLETED'), +(78, 4, 50, 29, 2, 8, '2026-02-10', '13:00:00', 'MISSED'), +(79, 4, 51, 30, 3, 13, '2026-02-12', '14:30:00', 'CANCELLED'), +(80, 5, 52, 31, 1, 6, '2026-02-14', '16:00:00', 'COMPLETED'), +(81, 1, 53, 32, 2, 7, '2026-02-16', '09:00:00', 'COMPLETED'), +(82, 2, 54, 33, 3, 12, '2026-02-18', '10:30:00', 'COMPLETED'), +(83, 3, 55, 34, 2, 9, '2026-02-20', '13:00:00', 'MISSED'), +(84, 4, 56, 35, 3, 14, '2026-02-22', '14:30:00', 'CANCELLED'), +(85, 5, 57, 36, 1, 3, '2026-02-24', '16:00:00', 'COMPLETED'), +(86, 4, 58, 37, 2, 8, '2026-02-26', '09:00:00', 'COMPLETED'), +(87, 4, 59, 38, 3, 13, '2026-02-28', '10:30:00', 'COMPLETED'), +(88, 2, 60, 39, 1, 6, '2026-03-02', '13:00:00', 'MISSED'), +(89, 2, 61, 40, 2, 7, '2026-03-04', '14:30:00', 'CANCELLED'), +(90, 5, 62, 41, 3, 12, '2026-03-06', '16:00:00', 'COMPLETED'); + +INSERT INTO adoption (adoptionId, petId, customerId, employeeId, sourceStoreId, adoptionDate, adoptionStatus) VALUES +(1, 37, 16, 3, 1, '2026-01-08', 'Completed'), +(2, 38, 17, 8, 2, '2026-01-13', 'Completed'), +(3, 39, 18, 13, 3, '2026-01-18', 'Completed'), +(4, 40, 19, 6, 1, '2026-01-23', 'Completed'), +(5, 41, 20, 7, 2, '2026-01-28', 'Completed'), +(6, 42, 21, 12, 3, '2026-02-02', 'Completed'), +(7, 43, 22, 5, 1, '2026-02-07', 'Completed'), +(8, 44, 23, 10, 2, '2026-02-12', 'Completed'), +(9, 45, 24, 11, 3, '2026-02-17', 'Completed'), +(10, 46, 25, 4, 1, '2026-02-22', 'Completed'), +(11, 47, 26, 9, 2, '2026-02-27', 'Completed'), +(12, 48, 27, 14, 3, '2026-03-04', 'Completed'), +(13, 49, 28, 3, 1, '2026-03-09', 'Completed'), +(14, 50, 29, 8, 2, '2026-03-14', 'Completed'), +(15, 51, 30, 13, 3, '2026-03-19', 'Completed'), +(16, 52, 31, 6, 1, '2026-03-24', 'Completed'), +(17, 53, 32, 7, 2, '2026-03-29', 'Completed'), +(18, 54, 33, 12, 3, '2026-04-03', 'Completed'); + +INSERT INTO cart (cartId, userId, storeId, couponId, cartStatus, subtotalAmount, discountAmount, totalAmount) VALUES +(1, 3, 1, 1, 'CHECKED_OUT', 44.51, 0.00, 44.51), +(2, 4, 1, 2, 'CHECKED_OUT', 252.00, 25.20, 226.80), +(3, 5, 1, 3, 'CHECKED_OUT', 432.33, 5.00, 427.33), +(4, 6, 1, 7, 'CHECKED_OUT', 108.67, 13.04, 95.63), +(5, 7, 2, 1, 'CHECKED_OUT', 512.62, 0.00, 512.62), +(6, 8, 2, 1, 'CHECKED_OUT', 481.83, 0.00, 481.83), +(7, 9, 2, 4, 'CHECKED_OUT', 78.06, 11.71, 66.35), +(8, 10, 2, 5, 'CHECKED_OUT', 212.94, 8.00, 204.94), +(9, 11, 3, 1, 'CHECKED_OUT', 729.31, 0.00, 729.31), +(10, 12, 3, 6, 'CHECKED_OUT', 221.87, 22.19, 199.68), +(11, 13, 3, 1, 'CHECKED_OUT', 162.19, 0.00, 162.19), +(12, 14, 3, 2, 'CHECKED_OUT', 593.40, 59.34, 534.06), +(13, 15, 1, 3, 'ACTIVE', 78.65, 5.00, 73.65), +(14, 16, 2, 7, 'ACTIVE', 140.29, 16.83, 123.46), +(15, 17, 3, 1, 'ACTIVE', 261.60, 0.00, 261.60), +(16, 18, 1, 1, 'ACTIVE', 93.84, 0.00, 93.84), +(17, 19, 2, 4, 'ACTIVE', 167.87, 25.18, 142.69), +(18, 20, 3, 5, 'ACTIVE', 206.96, 8.00, 198.96), +(19, 21, 1, 1, 'ACTIVE', 257.56, 0.00, 257.56), +(20, 22, 2, 6, 'ACTIVE', 258.59, 25.86, 232.73), +(21, 23, 3, 1, 'ACTIVE', 340.69, 0.00, 340.69), +(22, 24, 1, 2, 'ACTIVE', 152.52, 15.25, 137.27), +(23, 25, 2, 3, 'ACTIVE', 266.50, 5.00, 261.50), +(24, 26, 3, 7, 'ACTIVE', 231.01, 27.72, 203.29), +(25, 27, 1, 1, 'ACTIVE', 393.87, 0.00, 393.87), +(26, 28, 2, 1, 'ACTIVE', 437.53, 0.00, 437.53), +(27, 29, 3, 4, 'ACTIVE', 258.63, 38.79, 219.84), +(28, 30, 1, 5, 'ACTIVE', 132.17, 8.00, 124.17), +(29, 31, 2, 1, 'ACTIVE', 250.10, 0.00, 250.10), +(30, 32, 3, 6, 'ACTIVE', 498.48, 49.85, 448.63), +(31, 33, 1, 1, 'ABANDONED', 37.99, 0.00, 37.99), +(32, 34, 2, 2, 'ABANDONED', 318.98, 31.90, 287.08), +(33, 35, 3, 3, 'ABANDONED', 399.18, 5.00, 394.18), +(34, 36, 1, 7, 'ABANDONED', 79.36, 9.52, 69.84), +(35, 37, 2, 1, 'ABANDONED', 446.97, 0.00, 446.97), +(36, 38, 3, 1, 'ABANDONED', 714.69, 0.00, 714.69), +(37, 39, 1, 4, 'ABANDONED', 58.94, 0.00, 58.94), +(38, 40, 2, 5, 'ABANDONED', 248.85, 8.00, 240.85), +(39, 41, 3, 1, 'ABANDONED', 444.14, 0.00, 444.14), +(40, 42, 1, 6, 'ABANDONED', 178.85, 17.89, 160.96); + +INSERT INTO cart_item (cartItemId, cartId, prodId, quantity, unitPrice) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 2, 9.71), +(3, 2, 8, 2, 63.05), +(4, 2, 19, 3, 25.27), +(5, 2, 30, 1, 50.09), +(6, 3, 15, 3, 16.38), +(7, 3, 26, 1, 32.67), +(8, 3, 37, 2, 97.56), +(9, 3, 48, 3, 51.80), +(10, 4, 22, 1, 15.25), +(11, 4, 33, 2, 46.71), +(12, 5, 29, 2, 45.73), +(13, 5, 40, 3, 135.69), +(14, 5, 51, 1, 14.09), +(15, 6, 36, 3, 84.85), +(16, 6, 47, 1, 45.97), +(17, 6, 58, 2, 45.82), +(18, 6, 69, 3, 29.89), +(19, 7, 43, 1, 22.68), +(20, 7, 54, 2, 27.69), +(21, 8, 50, 2, 63.44), +(22, 8, 61, 3, 10.69), +(23, 8, 72, 1, 53.99), +(24, 9, 57, 3, 41.29), +(25, 9, 68, 1, 27.49), +(26, 9, 79, 2, 165.99), +(27, 9, 90, 3, 81.99), +(28, 10, 64, 1, 17.89), +(29, 10, 75, 2, 101.99), +(30, 11, 71, 2, 37.99), +(31, 11, 82, 3, 25.10), +(32, 11, 93, 1, 10.91), +(33, 12, 78, 3, 149.99), +(34, 12, 89, 1, 74.88), +(35, 12, 100, 2, 23.04), +(36, 12, 11, 3, 7.49), +(37, 13, 85, 1, 46.43), +(38, 13, 96, 2, 16.11), +(39, 14, 92, 2, 9.17), +(40, 14, 3, 3, 35.93), +(41, 14, 14, 1, 14.16), +(42, 15, 99, 3, 21.31), +(43, 15, 10, 1, 73.89), +(44, 15, 21, 2, 10.89), +(45, 15, 32, 3, 34.00), +(46, 16, 6, 1, 52.20), +(47, 16, 17, 2, 20.82), +(48, 17, 13, 2, 11.93), +(49, 17, 24, 3, 23.96), +(50, 17, 35, 1, 72.13), +(51, 18, 20, 3, 27.49), +(52, 18, 31, 1, 21.29), +(53, 18, 42, 2, 16.86), +(54, 18, 53, 3, 23.16), +(55, 19, 27, 1, 37.02), +(56, 19, 38, 2, 110.27), +(57, 20, 34, 2, 59.42), +(58, 20, 45, 3, 34.33), +(59, 20, 56, 1, 36.76), +(60, 21, 41, 3, 11.04), +(61, 21, 52, 1, 18.62), +(62, 21, 63, 2, 15.49), +(63, 21, 74, 3, 85.99), +(64, 22, 48, 1, 51.80), +(65, 22, 59, 2, 50.36), +(66, 23, 55, 2, 32.22), +(67, 23, 66, 3, 22.69), +(68, 23, 77, 1, 133.99), +(69, 24, 62, 3, 13.09), +(70, 24, 73, 1, 69.99), +(71, 24, 84, 2, 39.32), +(72, 24, 95, 3, 14.37), +(73, 25, 69, 1, 29.89), +(74, 25, 80, 2, 181.99), +(75, 26, 76, 2, 117.99), +(76, 26, 87, 3, 60.66), +(77, 26, 98, 1, 19.57), +(78, 27, 83, 3, 32.21), +(79, 27, 94, 1, 12.64), +(80, 27, 5, 2, 46.78), +(81, 27, 16, 3, 18.60), +(82, 28, 90, 1, 81.99), +(83, 28, 1, 2, 25.09), +(84, 29, 97, 2, 17.84), +(85, 29, 8, 3, 63.05), +(86, 29, 19, 1, 25.27), +(87, 30, 4, 3, 41.36), +(88, 30, 15, 1, 16.38), +(89, 30, 26, 2, 32.67), +(90, 30, 37, 3, 97.56), +(91, 31, 11, 1, 7.49), +(92, 31, 22, 2, 15.25), +(93, 32, 18, 2, 23.05), +(94, 32, 29, 3, 45.73), +(95, 32, 40, 1, 135.69), +(96, 33, 25, 3, 28.31), +(97, 33, 36, 1, 84.85), +(98, 33, 47, 2, 45.97), +(99, 33, 58, 3, 45.82), +(100, 34, 32, 1, 34.00), +(101, 34, 43, 2, 22.68), +(102, 35, 39, 2, 122.98), +(103, 35, 50, 3, 63.44), +(104, 35, 61, 1, 10.69), +(105, 36, 46, 3, 40.15), +(106, 36, 57, 1, 41.29), +(107, 36, 68, 2, 27.49), +(108, 36, 79, 3, 165.99), +(109, 37, 53, 1, 23.16), +(110, 37, 64, 2, 17.89), +(111, 38, 60, 2, 54.89), +(112, 38, 71, 3, 37.99), +(113, 38, 82, 1, 25.10), +(114, 39, 67, 3, 25.09), +(115, 39, 78, 1, 149.99), +(116, 39, 89, 2, 74.88), +(117, 39, 100, 3, 23.04), +(118, 40, 74, 1, 85.99), +(119, 40, 85, 2, 46.43); + +INSERT INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES +(1, '2026-02-02 10:11:00', 37.83, 'Cash', 3, 1, 3, 0, NULL, 'ONLINE', 1, 1, 44.51, 0.00, 6.68, 0), +(2, '2026-02-03 10:22:00', 192.78, 'Card', 4, 1, 4, 0, NULL, 'ONLINE', 2, 2, 252.00, 25.20, 34.02, 0), +(3, '2026-02-04 10:33:00', 363.23, 'Card', 5, 1, 5, 0, NULL, 'ONLINE', 3, 3, 432.33, 5.00, 64.10, 0), +(4, '2026-02-05 10:44:00', 81.29, 'Cash', 6, 1, 6, 0, NULL, 'ONLINE', 4, 7, 108.67, 13.04, 14.34, 0), +(5, '2026-02-06 10:55:00', 435.73, 'Card', 7, 2, 7, 0, NULL, 'ONLINE', 5, 1, 512.62, 0.00, 76.89, 0), +(6, '2026-02-07 11:06:00', 409.56, 'Card', 8, 2, 8, 0, NULL, 'ONLINE', 6, 1, 481.83, 0.00, 72.27, 0), +(7, '2026-02-08 11:17:00', 56.40, 'Cash', 9, 2, 9, 0, NULL, 'ONLINE', 7, 4, 78.06, 11.71, 9.95, 0), +(8, '2026-02-09 11:28:00', 174.20, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', 8, 5, 212.94, 8.00, 30.74, 0), +(9, '2026-02-10 11:39:00', 619.91, 'Card', 11, 3, 11, 0, NULL, 'ONLINE', 9, 1, 729.31, 0.00, 109.40, 0), +(10, '2026-02-11 11:50:00', 169.73, 'Card', 12, 3, 12, 0, NULL, 'ONLINE', 10, 6, 221.87, 22.19, 29.95, 0), +(11, '2026-02-12 12:01:00', 137.86, 'Cash', 13, 3, 13, 0, NULL, 'ONLINE', 11, 1, 162.19, 0.00, 24.33, 0), +(12, '2026-02-13 12:12:00', 453.95, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', 12, 2, 593.40, 59.34, 80.11, 0), +(13, '2026-01-05 09:15:00', 82.72, 'Card', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, 1, 82.72, 0.00, 0.00, 0), +(14, '2026-01-05 09:52:00', 120.43, 'Card', 8, 2, 16, 0, NULL, 'IN_STORE', NULL, 2, 133.81, 13.38, 0.00, 12), +(15, '2026-01-06 10:29:00', 153.21, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, 1, 153.21, 0.00, 0.00, 15), +(16, '2026-01-06 11:06:00', 20.27, 'Card', 6, 1, 18, 0, NULL, 'IN_STORE', NULL, 3, 25.27, 5.00, 0.00, 2), +(17, '2026-01-07 11:43:00', 58.96, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), +(18, '2026-01-07 12:20:00', 124.54, 'Card', 12, 3, 20, 0, NULL, 'IN_STORE', NULL, 7, 141.52, 16.98, 0.00, 12), +(19, '2026-01-08 12:57:00', 118.84, 'Card', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), +(20, '2026-01-08 13:34:00', 167.02, 'Cash', 10, 2, 22, 0, NULL, 'IN_STORE', NULL, 4, 196.50, 29.48, 0.00, 16), +(21, '2026-01-09 14:11:00', 367.69, 'Card', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), +(22, '2026-01-09 14:48:00', 57.62, 'Cash', 4, 1, 24, 0, NULL, 'IN_STORE', NULL, 1, 57.62, 0.00, 0.00, 5), +(23, '2026-01-10 15:25:00', 84.03, 'Card', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, 6, 93.37, 9.34, 0.00, 8), +(24, '2026-01-10 16:02:00', 297.25, 'Card', 14, 3, 26, 0, NULL, 'IN_STORE', NULL, 1, 297.25, 0.00, 0.00, 29), +(25, '2026-01-11 16:39:00', 35.78, 'Cash', 3, 1, 27, 0, NULL, 'IN_STORE', NULL, 2, 35.78, 0.00, 0.00, 3), +(26, '2026-01-11 17:16:00', 136.99, 'Card', 8, 2, 28, 0, NULL, 'IN_STORE', NULL, 1, 136.99, 0.00, 0.00, 13), +(27, '2026-01-12 17:53:00', 300.52, 'Cash', 13, 3, 29, 0, NULL, 'IN_STORE', NULL, 3, 305.52, 5.00, 0.00, 30), +(28, '2026-01-12 18:30:00', 165.99, 'Card', 6, 1, 30, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), +(29, '2026-01-13 19:07:00', 91.28, 'Card', 7, 2, 31, 0, NULL, 'IN_STORE', NULL, 7, 103.73, 12.45, 0.00, 9), +(30, '2026-01-13 19:44:00', 198.88, 'Cash', 12, 3, 32, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), +(31, '2026-01-14 20:21:00', 25.28, 'Card', 5, 1, 33, 0, NULL, 'IN_STORE', NULL, 4, 25.28, 0.00, 0.00, 2), +(32, '2026-01-14 20:58:00', 58.51, 'Cash', 10, 2, 34, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), +(33, '2026-01-15 21:35:00', 314.15, 'Card', 11, 3, 35, 0, NULL, 'IN_STORE', NULL, 1, 314.15, 0.00, 0.00, 31), +(34, '2026-01-15 22:12:00', 61.62, 'Card', 4, 1, 36, 0, NULL, 'IN_STORE', NULL, 6, 68.47, 6.85, 0.00, 6), +(35, '2026-01-16 22:49:00', 49.61, 'Cash', 9, 2, 37, 0, NULL, 'IN_STORE', NULL, 1, 49.61, 0.00, 0.00, 4), +(36, '2026-01-16 23:26:00', 196.32, 'Card', 14, 3, 38, 0, NULL, 'IN_STORE', NULL, 2, 218.13, 21.81, 0.00, 19), +(37, '2026-01-18 00:03:00', 47.92, 'Cash', 3, 1, 39, 0, NULL, 'IN_STORE', NULL, 1, 47.92, 0.00, 0.00, 4), +(38, '2026-01-18 00:40:00', 121.03, 'Card', 8, 2, 40, 0, NULL, 'IN_STORE', NULL, 3, 126.03, 5.00, 0.00, 12), +(39, '2026-01-19 01:17:00', 187.91, 'Card', 13, 3, 41, 0, NULL, 'IN_STORE', NULL, 1, 187.91, 0.00, 0.00, 18), +(40, '2026-01-19 01:54:00', 108.22, 'Cash', 6, 1, 42, 0, NULL, 'IN_STORE', NULL, 7, 122.98, 14.76, 0.00, 10), +(41, '2026-01-20 02:31:00', 67.71, 'Card', 7, 2, 43, 0, NULL, 'IN_STORE', NULL, 1, 67.71, 0.00, 0.00, 6), +(42, '2026-01-20 03:08:00', 114.93, 'Cash', 12, 3, 44, 0, NULL, 'IN_STORE', NULL, 4, 135.21, 20.28, 0.00, 11), +(43, '2026-01-21 03:45:00', 55.38, 'Card', 5, 1, 45, 0, NULL, 'IN_STORE', NULL, 1, 55.38, 0.00, 0.00, 5), +(44, '2026-01-21 04:22:00', 286.34, 'Card', 10, 2, 46, 0, NULL, 'IN_STORE', NULL, 1, 286.34, 0.00, 0.00, 28), +(45, '2026-01-22 04:59:00', 83.62, 'Cash', 11, 3, 47, 0, NULL, 'IN_STORE', NULL, 6, 92.91, 9.29, 0.00, 8), +(46, '2026-01-22 05:36:00', 29.89, 'Card', 4, 1, 48, 0, NULL, 'IN_STORE', NULL, 1, 29.89, 0.00, 0.00, 2), +(47, '2026-01-23 06:13:00', 161.48, 'Cash', 9, 2, 49, 0, NULL, 'IN_STORE', NULL, 2, 179.42, 17.94, 0.00, 16), +(48, '2026-01-23 06:50:00', 210.14, 'Card', 14, 3, 50, 0, NULL, 'IN_STORE', NULL, 1, 210.14, 0.00, 0.00, 21), +(49, '2026-01-24 07:27:00', 73.64, 'Card', 3, 1, 51, 0, NULL, 'IN_STORE', NULL, 3, 78.64, 5.00, 0.00, 7), +(50, '2026-01-24 08:04:00', 179.28, 'Cash', 8, 2, 52, 0, NULL, 'IN_STORE', NULL, 1, 179.28, 0.00, 0.00, 17), +(51, '2026-01-25 08:41:00', 101.67, 'Card', 13, 3, 53, 0, NULL, 'IN_STORE', NULL, 7, 115.53, 13.86, 0.00, 10), +(52, '2026-01-25 09:18:00', 21.31, 'Cash', 6, 1, 54, 0, NULL, 'IN_STORE', NULL, 1, 21.31, 0.00, 0.00, 2), +(53, '2026-01-26 09:55:00', 79.57, 'Card', 7, 2, 55, 0, NULL, 'IN_STORE', NULL, 4, 93.61, 14.04, 0.00, 7), +(54, '2026-01-26 10:32:00', 156.49, 'Card', 12, 3, 56, 0, NULL, 'IN_STORE', NULL, 1, 156.49, 0.00, 0.00, 15), +(55, '2026-01-27 11:09:00', 28.32, 'Cash', 5, 1, 57, 0, NULL, 'IN_STORE', NULL, 1, 28.32, 0.00, 0.00, 2), +(56, '2026-01-27 11:46:00', 175.47, 'Card', 10, 2, 58, 0, NULL, 'IN_STORE', NULL, 6, 194.97, 19.50, 0.00, 17), +(57, '2026-01-28 12:23:00', 150.60, 'Cash', 11, 3, 59, 0, NULL, 'IN_STORE', NULL, 1, 150.60, 0.00, 0.00, 15), +(58, '2026-01-28 13:00:00', 45.73, 'Card', 4, 1, 60, 0, NULL, 'IN_STORE', NULL, 2, 45.73, 0.00, 0.00, 4), +(59, '2026-01-29 13:37:00', 132.93, 'Card', 9, 2, 61, 0, NULL, 'IN_STORE', NULL, 1, 132.93, 0.00, 0.00, 13), +(60, '2026-01-29 14:14:00', 261.49, 'Cash', 14, 3, 62, 0, NULL, 'IN_STORE', NULL, 3, 266.49, 5.00, 0.00, 26), +(61, '2026-01-30 14:51:00', 57.02, 'Card', 3, 1, 63, 0, NULL, 'IN_STORE', NULL, 1, 57.02, 0.00, 0.00, 5), +(62, '2026-01-30 15:28:00', 90.64, 'Cash', 8, 2, 64, 0, NULL, 'IN_STORE', NULL, 7, 103.00, 12.36, 0.00, 9), +(63, '2026-01-31 16:05:00', 228.91, 'Card', 13, 3, 65, 0, NULL, 'IN_STORE', NULL, 1, 228.91, 0.00, 0.00, 22), +(64, '2026-01-31 16:42:00', 50.36, 'Card', 6, 1, 66, 0, NULL, 'IN_STORE', NULL, 4, 50.36, 0.00, 0.00, 5), +(65, '2026-02-01 17:19:00', 53.77, 'Cash', 7, 2, 67, 0, NULL, 'IN_STORE', NULL, 1, 53.77, 0.00, 0.00, 5), +(66, '2026-02-01 17:56:00', 172.92, 'Card', 12, 3, 68, 0, NULL, 'IN_STORE', NULL, 1, 172.92, 0.00, 0.00, 17), +(67, '2026-02-02 18:33:00', 154.78, 'Cash', 5, 1, 69, 0, NULL, 'IN_STORE', NULL, 6, 171.98, 17.20, 0.00, 15), +(68, '2026-02-02 19:10:00', 198.21, 'Card', 10, 2, 70, 0, NULL, 'IN_STORE', NULL, 1, 198.21, 0.00, 0.00, 19), +(69, '2026-02-03 19:47:00', 134.85, 'Card', 11, 3, 71, 0, NULL, 'IN_STORE', NULL, 2, 149.83, 14.98, 0.00, 13), +(70, '2026-02-03 20:24:00', 74.88, 'Cash', 4, 1, 72, 0, NULL, 'IN_STORE', NULL, 1, 74.88, 0.00, 0.00, 7), +(71, '2026-02-04 21:01:00', 27.77, 'Card', 9, 2, 73, 0, NULL, 'IN_STORE', NULL, 3, 32.77, 5.00, 0.00, 2), +(72, '2026-02-04 21:38:00', 105.22, 'Cash', 14, 3, 74, 0, NULL, 'IN_STORE', NULL, 1, 105.22, 0.00, 0.00, 10), +(73, '2026-02-05 22:15:00', 72.79, 'Card', 3, 1, 75, 0, NULL, 'IN_STORE', NULL, 7, 82.72, 9.93, 0.00, 7), +(74, '2026-02-05 22:52:00', 133.81, 'Card', 8, 2, 76, 0, NULL, 'IN_STORE', NULL, 1, 133.81, 0.00, 0.00, 13), +(75, '2026-02-06 23:29:00', 130.23, 'Cash', 13, 3, 77, 0, NULL, 'IN_STORE', NULL, 4, 153.21, 22.98, 0.00, 13), +(76, '2026-02-07 00:06:00', 25.27, 'Card', 6, 1, 78, 0, NULL, 'IN_STORE', NULL, 1, 25.27, 0.00, 0.00, 2), +(77, '2026-02-08 00:43:00', 58.96, 'Cash', 7, 2, 79, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), +(78, '2026-02-08 01:20:00', 127.37, 'Card', 12, 3, 80, 0, NULL, 'IN_STORE', NULL, 6, 141.52, 14.15, 0.00, 12), +(79, '2026-02-09 01:57:00', 118.84, 'Card', 5, 1, 81, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), +(80, '2026-02-09 02:34:00', 176.85, 'Cash', 10, 2, 82, 0, NULL, 'IN_STORE', NULL, 2, 196.50, 19.65, 0.00, 17), +(81, '2026-02-10 03:11:00', 367.69, 'Card', 11, 3, 83, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), +(82, '2026-02-10 03:48:00', 52.62, 'Cash', 4, 1, 84, 0, NULL, 'IN_STORE', NULL, 3, 57.62, 5.00, 0.00, 5), +(83, '2026-02-11 04:25:00', 93.37, 'Card', 9, 2, 85, 0, NULL, 'IN_STORE', NULL, 1, 93.37, 0.00, 0.00, 9), +(84, '2026-02-11 05:02:00', 261.58, 'Card', 14, 3, 86, 0, NULL, 'IN_STORE', NULL, 7, 297.25, 35.67, 0.00, 26), +(85, '2026-02-12 05:39:00', 35.78, 'Cash', 3, 1, 87, 0, NULL, 'IN_STORE', NULL, 1, 35.78, 0.00, 0.00, 3), +(86, '2026-02-12 06:16:00', 116.44, 'Card', 8, 2, 88, 0, NULL, 'IN_STORE', NULL, 4, 136.99, 20.55, 0.00, 11), +(87, '2026-02-13 06:53:00', 305.52, 'Cash', 13, 3, 89, 0, NULL, 'IN_STORE', NULL, 1, 305.52, 0.00, 0.00, 30), +(88, '2026-02-13 07:30:00', 165.99, 'Card', 6, 1, 90, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), +(89, '2026-02-14 08:07:00', 93.36, 'Card', 7, 2, 91, 0, NULL, 'IN_STORE', NULL, 6, 103.73, 10.37, 0.00, 9), +(90, '2026-02-14 08:44:00', 198.88, 'Cash', 12, 3, 92, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), +(91, '2026-02-15 09:21:00', 25.28, 'Card', 5, 1, 93, 0, NULL, 'IN_STORE', NULL, 2, 25.28, 0.00, 0.00, 2), +(92, '2026-02-15 09:58:00', 58.51, 'Cash', 10, 2, 94, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), +(93, '2026-02-16 10:35:00', 309.15, 'Card', 11, 3, 95, 0, NULL, 'IN_STORE', NULL, 3, 314.15, 5.00, 0.00, 30), +(94, '2026-02-16 11:12:00', 68.47, 'Card', 4, 1, 96, 0, NULL, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 6), +(95, '2026-02-17 11:49:00', 49.61, 'Cash', 9, 2, 97, 0, NULL, 'IN_STORE', NULL, 7, 49.61, 0.00, 0.00, 4), +(96, '2026-02-13 10:11:00', 34.80, 'Card', 3, 1, 3, 1, 1, 'IN_STORE', NULL, 1, 34.80, 0.00, 0.00, 0), +(97, '2026-02-15 10:22:00', 88.32, 'Card', 4, 1, 4, 1, 2, 'IN_STORE', NULL, 1, 88.32, 0.00, 0.00, 0), +(98, '2026-02-17 10:33:00', 49.05, 'Card', 5, 1, 5, 1, 3, 'IN_STORE', NULL, 1, 49.05, 0.00, 0.00, 0), +(99, '2026-02-19 10:44:00', 15.25, 'Card', 6, 1, 6, 1, 4, 'IN_STORE', NULL, 1, 15.25, 0.00, 0.00, 0), +(100, '2026-02-21 10:55:00', 181.42, 'Card', 7, 2, 7, 1, 5, 'IN_STORE', NULL, 1, 181.42, 0.00, 0.00, 0), +(101, '2026-02-23 11:06:00', 130.82, 'Card', 8, 2, 8, 1, 6, 'IN_STORE', NULL, 1, 130.82, 0.00, 0.00, 0), +(102, '2026-02-25 11:17:00', 50.37, 'Card', 9, 2, 9, 1, 7, 'IN_STORE', NULL, 1, 50.37, 0.00, 0.00, 0), +(103, '2026-02-27 11:28:00', 74.13, 'Card', 10, 2, 10, 1, 8, 'IN_STORE', NULL, 1, 74.13, 0.00, 0.00, 0), +(104, '2026-03-01 11:39:00', 68.78, 'Card', 11, 3, 11, 1, 9, 'IN_STORE', NULL, 1, 68.78, 0.00, 0.00, 0), +(105, '2026-03-03 11:50:00', 17.89, 'Card', 12, 3, 12, 1, 10, 'IN_STORE', NULL, 1, 17.89, 0.00, 0.00, 0), +(106, '2026-03-05 12:01:00', 63.09, 'Card', 13, 3, 13, 1, 11, 'IN_STORE', NULL, 1, 63.09, 0.00, 0.00, 0), +(107, '2026-03-07 12:12:00', 149.99, 'Card', 14, 3, 14, 1, 12, 'IN_STORE', NULL, 1, 149.99, 0.00, 0.00, 0), +(108, '2026-01-28 09:15:00', 41.36, 'Card', 3, 1, 15, 1, 13, 'IN_STORE', NULL, 1, 41.36, 0.00, 0.00, 0), +(109, '2026-01-29 09:52:00', 68.47, 'Card', 8, 2, 16, 1, 14, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 0), +(110, '2026-01-31 10:29:00', 14.16, 'Card', 13, 3, 17, 1, 15, 'IN_STORE', NULL, 1, 14.16, 0.00, 0.00, 0); + +INSERT INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 2, 9.71), +(3, 2, 8, 2, 63.05), +(4, 2, 19, 3, 25.27), +(5, 2, 30, 1, 50.09), +(6, 3, 15, 3, 16.38), +(7, 3, 26, 1, 32.67), +(8, 3, 37, 2, 97.56), +(9, 3, 48, 3, 51.80), +(10, 4, 22, 1, 15.25), +(11, 4, 33, 2, 46.71), +(12, 5, 29, 2, 45.73), +(13, 5, 40, 3, 135.69), +(14, 5, 51, 1, 14.09), +(15, 6, 36, 3, 84.85), +(16, 6, 47, 1, 45.97), +(17, 6, 58, 2, 45.82), +(18, 6, 69, 3, 29.89), +(19, 7, 43, 1, 22.68), +(20, 7, 54, 2, 27.69), +(21, 8, 50, 2, 63.44), +(22, 8, 61, 3, 10.69), +(23, 8, 72, 1, 53.99), +(24, 9, 57, 3, 41.29), +(25, 9, 68, 1, 27.49), +(26, 9, 79, 2, 165.99), +(27, 9, 90, 3, 81.99), +(28, 10, 64, 1, 17.89), +(29, 10, 75, 2, 101.99), +(30, 11, 71, 2, 37.99), +(31, 11, 82, 3, 25.10), +(32, 11, 93, 1, 10.91), +(33, 12, 78, 3, 149.99), +(34, 12, 89, 1, 74.88), +(35, 12, 100, 2, 23.04), +(36, 12, 11, 3, 7.49), +(37, 13, 4, 2, 41.36), +(38, 14, 9, 1, 68.47), +(39, 14, 26, 2, 32.67), +(40, 15, 14, 2, 14.16), +(41, 15, 31, 1, 21.29), +(42, 15, 48, 2, 51.80), +(43, 16, 19, 1, 25.27), +(44, 17, 24, 2, 23.96), +(45, 17, 41, 1, 11.04), +(46, 18, 29, 1, 45.73), +(47, 18, 46, 2, 40.15), +(48, 18, 63, 1, 15.49), +(49, 19, 34, 2, 59.42), +(50, 20, 39, 1, 122.98), +(51, 20, 56, 2, 36.76), +(52, 21, 44, 2, 28.51), +(53, 21, 61, 1, 10.69), +(54, 21, 78, 2, 149.99), +(55, 22, 49, 1, 57.62), +(56, 23, 54, 2, 27.69), +(57, 23, 71, 1, 37.99), +(58, 24, 59, 1, 50.36), +(59, 24, 76, 2, 117.99), +(60, 24, 93, 1, 10.91), +(61, 25, 64, 2, 17.89), +(62, 26, 69, 1, 29.89), +(63, 26, 86, 2, 53.55), +(64, 27, 74, 2, 85.99), +(65, 27, 91, 1, 7.44), +(66, 27, 8, 2, 63.05), +(67, 28, 79, 1, 165.99), +(68, 29, 84, 2, 39.32), +(69, 29, 1, 1, 25.09), +(70, 30, 89, 1, 74.88), +(71, 30, 6, 2, 52.20), +(72, 30, 23, 1, 19.60), +(73, 31, 94, 2, 12.64), +(74, 32, 99, 1, 21.31), +(75, 32, 16, 2, 18.60), +(76, 33, 4, 2, 41.36), +(77, 33, 21, 1, 10.89), +(78, 33, 38, 2, 110.27), +(79, 34, 9, 1, 68.47), +(80, 35, 14, 2, 14.16), +(81, 35, 31, 1, 21.29), +(82, 36, 19, 1, 25.27), +(83, 36, 36, 2, 84.85), +(84, 36, 53, 1, 23.16), +(85, 37, 24, 2, 23.96), +(86, 38, 29, 1, 45.73), +(87, 38, 46, 2, 40.15), +(88, 39, 34, 2, 59.42), +(89, 39, 51, 1, 14.09), +(90, 39, 68, 2, 27.49), +(91, 40, 39, 1, 122.98), +(92, 41, 44, 2, 28.51), +(93, 41, 61, 1, 10.69), +(94, 42, 49, 1, 57.62), +(95, 42, 66, 2, 22.69), +(96, 42, 83, 1, 32.21), +(97, 43, 54, 2, 27.69), +(98, 44, 59, 1, 50.36), +(99, 44, 76, 2, 117.99), +(100, 45, 64, 2, 17.89), +(101, 45, 81, 1, 17.99), +(102, 45, 98, 2, 19.57), +(103, 46, 69, 1, 29.89), +(104, 47, 74, 2, 85.99), +(105, 47, 91, 1, 7.44), +(106, 48, 79, 1, 165.99), +(107, 48, 96, 2, 16.11), +(108, 48, 13, 1, 11.93), +(109, 49, 84, 2, 39.32), +(110, 50, 89, 1, 74.88), +(111, 50, 6, 2, 52.20), +(112, 51, 94, 2, 12.64), +(113, 51, 11, 1, 7.49), +(114, 51, 28, 2, 41.38), +(115, 52, 99, 1, 21.31), +(116, 53, 4, 2, 41.36), +(117, 53, 21, 1, 10.89), +(118, 54, 9, 1, 68.47), +(119, 54, 26, 2, 32.67), +(120, 54, 43, 1, 22.68), +(121, 55, 14, 2, 14.16), +(122, 56, 19, 1, 25.27), +(123, 56, 36, 2, 84.85), +(124, 57, 24, 2, 23.96), +(125, 57, 41, 1, 11.04), +(126, 57, 58, 2, 45.82), +(127, 58, 29, 1, 45.73), +(128, 59, 34, 2, 59.42), +(129, 59, 51, 1, 14.09), +(130, 60, 39, 1, 122.98), +(131, 60, 56, 2, 36.76), +(132, 60, 73, 1, 69.99), +(133, 61, 44, 2, 28.51), +(134, 62, 49, 1, 57.62), +(135, 62, 66, 2, 22.69), +(136, 63, 54, 2, 27.69), +(137, 63, 71, 1, 37.99), +(138, 63, 88, 2, 67.77), +(139, 64, 59, 1, 50.36), +(140, 65, 64, 2, 17.89), +(141, 65, 81, 1, 17.99), +(142, 66, 69, 1, 29.89), +(143, 66, 86, 2, 53.55), +(144, 66, 3, 1, 35.93), +(145, 67, 74, 2, 85.99), +(146, 68, 79, 1, 165.99), +(147, 68, 96, 2, 16.11), +(148, 69, 84, 2, 39.32), +(149, 69, 1, 1, 25.09), +(150, 69, 18, 2, 23.05), +(151, 70, 89, 1, 74.88), +(152, 71, 94, 2, 12.64), +(153, 71, 11, 1, 7.49), +(154, 72, 99, 1, 21.31), +(155, 72, 16, 2, 18.60), +(156, 72, 33, 1, 46.71), +(157, 73, 4, 2, 41.36), +(158, 74, 9, 1, 68.47), +(159, 74, 26, 2, 32.67), +(160, 75, 14, 2, 14.16), +(161, 75, 31, 1, 21.29), +(162, 75, 48, 2, 51.80), +(163, 76, 19, 1, 25.27), +(164, 77, 24, 2, 23.96), +(165, 77, 41, 1, 11.04), +(166, 78, 29, 1, 45.73), +(167, 78, 46, 2, 40.15), +(168, 78, 63, 1, 15.49), +(169, 79, 34, 2, 59.42), +(170, 80, 39, 1, 122.98), +(171, 80, 56, 2, 36.76), +(172, 81, 44, 2, 28.51), +(173, 81, 61, 1, 10.69), +(174, 81, 78, 2, 149.99), +(175, 82, 49, 1, 57.62), +(176, 83, 54, 2, 27.69), +(177, 83, 71, 1, 37.99), +(178, 84, 59, 1, 50.36), +(179, 84, 76, 2, 117.99), +(180, 84, 93, 1, 10.91), +(181, 85, 64, 2, 17.89), +(182, 86, 69, 1, 29.89), +(183, 86, 86, 2, 53.55), +(184, 87, 74, 2, 85.99), +(185, 87, 91, 1, 7.44), +(186, 87, 8, 2, 63.05), +(187, 88, 79, 1, 165.99), +(188, 89, 84, 2, 39.32), +(189, 89, 1, 1, 25.09), +(190, 90, 89, 1, 74.88), +(191, 90, 6, 2, 52.20), +(192, 90, 23, 1, 19.60), +(193, 91, 94, 2, 12.64), +(194, 92, 99, 1, 21.31), +(195, 92, 16, 2, 18.60), +(196, 93, 4, 2, 41.36), +(197, 93, 21, 1, 10.89), +(198, 93, 38, 2, 110.27), +(199, 94, 9, 1, 68.47), +(200, 95, 14, 2, 14.16), +(201, 95, 31, 1, 21.29), +(202, 96, 1, 1, 25.09), +(203, 96, 12, 1, 9.71), +(204, 97, 8, 1, 63.05), +(205, 97, 19, 1, 25.27), +(206, 98, 15, 1, 16.38), +(207, 98, 26, 1, 32.67), +(208, 99, 22, 1, 15.25), +(209, 100, 29, 1, 45.73), +(210, 100, 40, 1, 135.69), +(211, 101, 36, 1, 84.85), +(212, 101, 47, 1, 45.97), +(213, 102, 43, 1, 22.68), +(214, 102, 54, 1, 27.69), +(215, 103, 50, 1, 63.44), +(216, 103, 61, 1, 10.69), +(217, 104, 57, 1, 41.29), +(218, 104, 68, 1, 27.49), +(219, 105, 64, 1, 17.89), +(220, 106, 71, 1, 37.99), +(221, 106, 82, 1, 25.10), +(222, 107, 78, 1, 149.99), +(223, 108, 4, 1, 41.36), +(224, 109, 9, 1, 68.47), +(225, 110, 14, 1, 14.16); + +INSERT INTO refund (id, saleId, customerId, amount, reason, status) VALUES +(1, 1, 3, 34.80, 'Product was unsuitable after purchase.', 'APPROVED'), +(2, 2, 4, 88.32, 'Duplicate item purchased in error.', 'APPROVED'), +(3, 3, 5, 49.05, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(4, 4, 6, 15.25, 'Food sensitivity required a different formula.', 'APPROVED'), +(5, 5, 7, 181.42, 'Customer reported damaged packaging.', 'APPROVED'), +(6, 6, 8, 130.82, 'Product was unsuitable after purchase.', 'APPROVED'), +(7, 7, 9, 50.37, 'Duplicate item purchased in error.', 'APPROVED'), +(8, 8, 10, 74.13, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(9, 9, 11, 68.78, 'Food sensitivity required a different formula.', 'APPROVED'), +(10, 10, 12, 17.89, 'Customer reported damaged packaging.', 'APPROVED'), +(11, 11, 13, 63.09, 'Product was unsuitable after purchase.', 'APPROVED'), +(12, 12, 14, 149.99, 'Duplicate item purchased in error.', 'APPROVED'), +(13, 13, 15, 41.36, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(14, 14, 16, 68.47, 'Food sensitivity required a different formula.', 'APPROVED'), +(15, 15, 17, 14.16, 'Customer reported damaged packaging.', 'APPROVED'); + +INSERT INTO refund_item (id, refund_id, prod_id, quantity, unit_price) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 1, 9.71), +(3, 2, 8, 1, 63.05), +(4, 2, 19, 1, 25.27), +(5, 3, 15, 1, 16.38), +(6, 3, 26, 1, 32.67), +(7, 4, 22, 1, 15.25), +(8, 5, 29, 1, 45.73), +(9, 5, 40, 1, 135.69), +(10, 6, 36, 1, 84.85), +(11, 6, 47, 1, 45.97), +(12, 7, 43, 1, 22.68), +(13, 7, 54, 1, 27.69), +(14, 8, 50, 1, 63.44), +(15, 8, 61, 1, 10.69), +(16, 9, 57, 1, 41.29), +(17, 9, 68, 1, 27.49), +(18, 10, 64, 1, 17.89), +(19, 11, 71, 1, 37.99), +(20, 11, 82, 1, 25.10), +(21, 12, 78, 1, 149.99), +(22, 13, 4, 1, 41.36), +(23, 14, 9, 1, 68.47), +(24, 15, 14, 1, 14.16); + +INSERT INTO conversation (id, customerId, staffId, status, mode, humanRequestedAt) VALUES +(1, 16, 3, 'CLOSED', 'AUTOMATED', NULL), +(2, 17, 4, 'OPEN', 'HUMAN', '2026-02-02 09:08:00'), +(3, 18, 5, 'OPEN', 'HUMAN', '2026-02-03 09:08:00'), +(4, 19, 6, 'OPEN', 'AUTOMATED', NULL), +(5, 20, 7, 'CLOSED', 'HUMAN', '2026-02-05 09:08:00'), +(6, 21, 8, 'OPEN', 'HUMAN', '2026-02-06 09:08:00'), +(7, 22, 9, 'OPEN', 'AUTOMATED', NULL), +(8, 23, 10, 'OPEN', 'HUMAN', '2026-02-08 09:08:00'), +(9, 24, 11, 'CLOSED', 'HUMAN', '2026-02-09 09:08:00'), +(10, 25, 12, 'OPEN', 'AUTOMATED', NULL), +(11, 26, 13, 'OPEN', 'HUMAN', '2026-02-11 09:08:00'), +(12, 27, 14, 'OPEN', 'HUMAN', '2026-02-12 09:08:00'), +(13, 28, 3, 'CLOSED', 'AUTOMATED', NULL), +(14, 29, 4, 'OPEN', 'HUMAN', '2026-02-14 09:08:00'), +(15, 30, 5, 'OPEN', 'HUMAN', '2026-02-15 09:08:00'), +(16, 31, 6, 'OPEN', 'AUTOMATED', NULL), +(17, 32, 7, 'CLOSED', 'HUMAN', '2026-02-17 09:08:00'), +(18, 33, 8, 'OPEN', 'HUMAN', '2026-02-18 09:08:00'), +(19, 34, 9, 'OPEN', 'AUTOMATED', NULL), +(20, 35, 10, 'OPEN', 'HUMAN', '2026-02-20 09:08:00'), +(21, 36, 11, 'CLOSED', 'HUMAN', '2026-02-21 09:08:00'), +(22, 37, 12, 'OPEN', 'AUTOMATED', NULL), +(23, 38, 13, 'OPEN', 'HUMAN', '2026-02-23 09:08:00'), +(24, 39, 14, 'OPEN', 'HUMAN', '2026-02-24 09:08:00'), +(25, 40, 3, 'CLOSED', 'AUTOMATED', NULL), +(26, 41, 4, 'OPEN', 'HUMAN', '2026-02-26 09:08:00'), +(27, 42, 5, 'OPEN', 'HUMAN', '2026-02-27 09:08:00'), +(28, 43, 6, 'OPEN', 'AUTOMATED', NULL), +(29, 44, 7, 'CLOSED', 'HUMAN', '2026-03-01 09:08:00'), +(30, 45, 8, 'OPEN', 'HUMAN', '2026-03-02 09:08:00'); + +INSERT INTO message (id, conversationId, senderId, content, attachmentUrl, attachmentName, attachmentMimeType, attachmentSizeBytes, timestamp, isRead) VALUES +(1, 1, 16, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-01 09:00:00', 1), +(2, 1, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-01 09:05:00', 1), +(3, 1, 16, 'Order #1001 is the one I meant, and the pet is Bruno.', 'https://files.petshop.local/chat/001-2.pdf', 'order-note-001.pdf', 'application/pdf', 145000, '2026-02-01 09:10:00', 1), +(4, 1, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-01 09:15:00', 0), +(5, 2, 17, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-02 09:00:00', 1), +(6, 2, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-02 09:05:00', 1), +(7, 2, 17, 'Order #1002 is the one I meant, and the pet is Snowball.', NULL, NULL, NULL, NULL, '2026-02-02 09:10:00', 1), +(8, 2, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-02 09:15:00', 0), +(9, 3, 18, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-03 09:00:00', 1), +(10, 3, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-03 09:05:00', 1), +(11, 3, 18, 'Order #1003 is the one I meant, and the pet is Zeus.', NULL, NULL, NULL, NULL, '2026-02-03 09:10:00', 1), +(12, 3, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-03 09:15:00', 0), +(13, 4, 19, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-04 09:00:00', 1), +(14, 4, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-04 09:05:00', 1), +(15, 4, 19, 'Order #1004 is the one I meant, and the pet is Biscuit.', NULL, NULL, NULL, NULL, '2026-02-04 09:10:00', 1), +(16, 4, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-04 09:15:00', 0), +(17, 5, 20, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-05 09:00:00', 1), +(18, 5, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-05 09:05:00', 1), +(19, 5, 20, 'Order #1005 is the one I meant, and the pet is Patches.', NULL, NULL, NULL, NULL, '2026-02-05 09:10:00', 1), +(20, 5, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-05 09:15:00', 0), +(21, 6, 21, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-06 09:00:00', 1), +(22, 6, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-06 09:05:00', 1), +(23, 6, 21, 'Order #1006 is the one I meant, and the pet is Scout.', 'https://files.petshop.local/chat/006-2.pdf', 'order-note-006.pdf', 'application/pdf', 145500, '2026-02-06 09:10:00', 1), +(24, 6, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-06 09:15:00', 0), +(25, 7, 22, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-07 09:00:00', 1), +(26, 7, 9, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-07 09:05:00', 1), +(27, 7, 22, 'Order #1007 is the one I meant, and the pet is Mittens.', NULL, NULL, NULL, NULL, '2026-02-07 09:10:00', 1), +(28, 7, 9, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-07 09:15:00', 0), +(29, 8, 23, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-08 09:00:00', 1), +(30, 8, 10, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-08 09:05:00', 1), +(31, 8, 23, 'Order #1008 is the one I meant, and the pet is Thor.', NULL, NULL, NULL, NULL, '2026-02-08 09:10:00', 1), +(32, 8, 10, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-08 09:15:00', 0), +(33, 9, 24, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-09 09:00:00', 1), +(34, 9, 11, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-09 09:05:00', 1), +(35, 9, 24, 'Order #1009 is the one I meant, and the pet is Whiskers.', NULL, NULL, NULL, NULL, '2026-02-09 09:10:00', 1), +(36, 9, 11, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-09 09:15:00', 0), +(37, 10, 25, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-10 09:00:00', 1), +(38, 10, 12, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-10 09:05:00', 1), +(39, 10, 25, 'Order #1010 is the one I meant, and the pet is Goldie.', NULL, NULL, NULL, NULL, '2026-02-10 09:10:00', 1), +(40, 10, 12, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-10 09:15:00', 0), +(41, 11, 26, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-11 09:00:00', 1), +(42, 11, 13, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-11 09:05:00', 1), +(43, 11, 26, 'Order #1011 is the one I meant, and the pet is Midnight.', 'https://files.petshop.local/chat/011-2.pdf', 'order-note-011.pdf', 'application/pdf', 146000, '2026-02-11 09:10:00', 1), +(44, 11, 13, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-11 09:15:00', 0), +(45, 12, 27, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-12 09:00:00', 1), +(46, 12, 14, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-12 09:05:00', 1), +(47, 12, 27, 'Order #1012 is the one I meant, and the pet is Storm.', NULL, NULL, NULL, NULL, '2026-02-12 09:10:00', 1), +(48, 12, 14, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-12 09:15:00', 0), +(49, 13, 28, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-13 09:00:00', 1), +(50, 13, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-13 09:05:00', 1), +(51, 13, 28, 'Order #1013 is the one I meant, and the pet is Peanut.', NULL, NULL, NULL, NULL, '2026-02-13 09:10:00', 1), +(52, 13, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-13 09:15:00', 0), +(53, 14, 29, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-14 09:00:00', 1), +(54, 14, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-14 09:05:00', 1), +(55, 14, 29, 'Order #1014 is the one I meant, and the pet is Daisy.', NULL, NULL, NULL, NULL, '2026-02-14 09:10:00', 1), +(56, 14, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-14 09:15:00', 0), +(57, 15, 30, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-15 09:00:00', 1), +(58, 15, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-15 09:05:00', 1), +(59, 15, 30, 'Order #1015 is the one I meant, and the pet is Cleo.', NULL, NULL, NULL, NULL, '2026-02-15 09:10:00', 1), +(60, 15, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-15 09:15:00', 0), +(61, 16, 31, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-16 09:00:00', 1), +(62, 16, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-16 09:05:00', 1), +(63, 16, 31, 'Order #1016 is the one I meant, and the pet is Sunny.', 'https://files.petshop.local/chat/016-2.pdf', 'order-note-016.pdf', 'application/pdf', 146500, '2026-02-16 09:10:00', 1), +(64, 16, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-16 09:15:00', 0), +(65, 17, 32, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-17 09:00:00', 1), +(66, 17, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-17 09:05:00', 1), +(67, 17, 32, 'Order #1017 is the one I meant, and the pet is Maple.', NULL, NULL, NULL, NULL, '2026-02-17 09:10:00', 1), +(68, 17, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-17 09:15:00', 0), +(69, 18, 33, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-18 09:00:00', 1), +(70, 18, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-18 09:05:00', 1), +(71, 18, 33, 'Order #1018 is the one I meant, and the pet is Nova.', NULL, NULL, NULL, NULL, '2026-02-18 09:10:00', 1), +(72, 18, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-18 09:15:00', 0), +(73, 19, 34, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-19 09:00:00', 1), +(74, 19, 9, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-19 09:05:00', 1), +(75, 19, 34, 'Order #1019 is the one I meant, and the pet is Piper.', NULL, NULL, NULL, NULL, '2026-02-19 09:10:00', 1), +(76, 19, 9, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-19 09:15:00', 0), +(77, 20, 35, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-20 09:00:00', 1), +(78, 20, 10, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-20 09:05:00', 1), +(79, 20, 35, 'Order #1020 is the one I meant, and the pet is Hazel.', NULL, NULL, NULL, NULL, '2026-02-20 09:10:00', 1), +(80, 20, 10, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-20 09:15:00', 0), +(81, 21, 36, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-21 09:00:00', 1), +(82, 21, 11, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-21 09:05:00', 1), +(83, 21, 36, 'Order #1021 is the one I meant, and the pet is Jasper.', 'https://files.petshop.local/chat/021-2.pdf', 'order-note-021.pdf', 'application/pdf', 147000, '2026-02-21 09:10:00', 1), +(84, 21, 11, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-21 09:15:00', 0), +(85, 22, 37, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-22 09:00:00', 1), +(86, 22, 12, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-22 09:05:00', 1), +(87, 22, 37, 'Order #1022 is the one I meant, and the pet is Remy.', NULL, NULL, NULL, NULL, '2026-02-22 09:10:00', 1), +(88, 22, 12, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-22 09:15:00', 0), +(89, 23, 38, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-23 09:00:00', 1), +(90, 23, 13, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-23 09:05:00', 1), +(91, 23, 38, 'Order #1023 is the one I meant, and the pet is Archie.', NULL, NULL, NULL, NULL, '2026-02-23 09:10:00', 1), +(92, 23, 13, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-23 09:15:00', 0), +(93, 24, 39, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-24 09:00:00', 1), +(94, 24, 14, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-24 09:05:00', 1), +(95, 24, 39, 'Order #1024 is the one I meant, and the pet is Skye.', NULL, NULL, NULL, NULL, '2026-02-24 09:10:00', 1), +(96, 24, 14, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-24 09:15:00', 0), +(97, 25, 40, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-25 09:00:00', 1), +(98, 25, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-25 09:05:00', 1), +(99, 25, 40, 'Order #1025 is the one I meant, and the pet is Otis.', NULL, NULL, NULL, NULL, '2026-02-25 09:10:00', 1), +(100, 25, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-25 09:15:00', 0), +(101, 26, 41, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-26 09:00:00', 1), +(102, 26, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-26 09:05:00', 1), +(103, 26, 41, 'Order #1026 is the one I meant, and the pet is Marley.', 'https://files.petshop.local/chat/026-2.pdf', 'order-note-026.pdf', 'application/pdf', 147500, '2026-02-26 09:10:00', 1), +(104, 26, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-26 09:15:00', 0), +(105, 27, 42, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-27 09:00:00', 1), +(106, 27, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-27 09:05:00', 1), +(107, 27, 42, 'Order #1027 is the one I meant, and the pet is Blue.', NULL, NULL, NULL, NULL, '2026-02-27 09:10:00', 1), +(108, 27, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-27 09:15:00', 0), +(109, 28, 43, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-28 09:00:00', 1), +(110, 28, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-28 09:05:00', 1), +(111, 28, 43, 'Order #1028 is the one I meant, and the pet is Honey.', NULL, NULL, NULL, NULL, '2026-02-28 09:10:00', 1), +(112, 28, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-28 09:15:00', 0), +(113, 29, 44, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-03-01 09:00:00', 1), +(114, 29, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-01 09:05:00', 1), +(115, 29, 44, 'Order #1029 is the one I meant, and the pet is Mochi.', NULL, NULL, NULL, NULL, '2026-03-01 09:10:00', 1), +(116, 29, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-01 09:15:00', 0), +(117, 30, 45, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-03-02 09:00:00', 1), +(118, 30, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-02 09:05:00', 1), +(119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1), +(120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0); + +INSERT INTO activityLog (logId, userId, storeId, activity, logTimestamp) VALUES +(1, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-03 08:00:00'), +(2, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-03 17:00:00'), +(3, 3, 1, 'Updated a pet availability record.', '2026-01-04 02:00:00'), +(4, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-04 11:00:00'), +(5, 5, 1, 'Checked a pending adoption record.', '2026-01-04 20:00:00'), +(6, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-05 05:00:00'), +(7, 7, 2, 'Answered a customer support conversation.', '2026-01-05 14:00:00'), +(8, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-05 23:00:00'), +(9, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-06 08:00:00'), +(10, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-06 17:00:00'), +(11, 11, 3, 'Updated a pet availability record.', '2026-01-07 02:00:00'), +(12, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-07 11:00:00'), +(13, 13, 3, 'Checked a pending adoption record.', '2026-01-07 20:00:00'), +(14, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-08 05:00:00'), +(15, 1, 1, 'Answered a customer support conversation.', '2026-01-08 14:00:00'), +(16, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-08 23:00:00'), +(17, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-09 08:00:00'), +(18, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-09 17:00:00'), +(19, 5, 1, 'Updated a pet availability record.', '2026-01-10 02:00:00'), +(20, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-10 11:00:00'), +(21, 7, 2, 'Checked a pending adoption record.', '2026-01-10 20:00:00'), +(22, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-11 05:00:00'), +(23, 9, 2, 'Answered a customer support conversation.', '2026-01-11 14:00:00'), +(24, 10, 2, 'Updated a product detail for the catalogue.', '2026-01-11 23:00:00'), +(25, 11, 3, 'Reviewed store inventory adjustments.', '2026-01-12 08:00:00'), +(26, 12, 3, 'Approved a purchase transaction at the register.', '2026-01-12 17:00:00'), +(27, 13, 3, 'Updated a pet availability record.', '2026-01-13 02:00:00'), +(28, 14, 3, 'Completed a grooming appointment handoff.', '2026-01-13 11:00:00'), +(29, 1, 1, 'Checked a pending adoption record.', '2026-01-13 20:00:00'), +(30, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-14 05:00:00'), +(31, 3, 1, 'Answered a customer support conversation.', '2026-01-14 14:00:00'), +(32, 4, 1, 'Updated a product detail for the catalogue.', '2026-01-14 23:00:00'), +(33, 5, 1, 'Reviewed store inventory adjustments.', '2026-01-15 08:00:00'), +(34, 6, 1, 'Approved a purchase transaction at the register.', '2026-01-15 17:00:00'), +(35, 7, 2, 'Updated a pet availability record.', '2026-01-16 02:00:00'), +(36, 8, 2, 'Completed a grooming appointment handoff.', '2026-01-16 11:00:00'), +(37, 9, 2, 'Checked a pending adoption record.', '2026-01-16 20:00:00'), +(38, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-17 05:00:00'), +(39, 11, 3, 'Answered a customer support conversation.', '2026-01-17 14:00:00'), +(40, 12, 3, 'Updated a product detail for the catalogue.', '2026-01-17 23:00:00'), +(41, 13, 3, 'Reviewed store inventory adjustments.', '2026-01-18 08:00:00'), +(42, 14, 3, 'Approved a purchase transaction at the register.', '2026-01-18 17:00:00'), +(43, 1, 1, 'Updated a pet availability record.', '2026-01-19 02:00:00'), +(44, 2, 2, 'Completed a grooming appointment handoff.', '2026-01-19 11:00:00'), +(45, 3, 1, 'Checked a pending adoption record.', '2026-01-19 20:00:00'), +(46, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-20 05:00:00'), +(47, 5, 1, 'Answered a customer support conversation.', '2026-01-20 14:00:00'), +(48, 6, 1, 'Updated a product detail for the catalogue.', '2026-01-20 23:00:00'), +(49, 7, 2, 'Reviewed store inventory adjustments.', '2026-01-21 08:00:00'), +(50, 8, 2, 'Approved a purchase transaction at the register.', '2026-01-21 17:00:00'), +(51, 9, 2, 'Updated a pet availability record.', '2026-01-22 02:00:00'), +(52, 10, 2, 'Completed a grooming appointment handoff.', '2026-01-22 11:00:00'), +(53, 11, 3, 'Checked a pending adoption record.', '2026-01-22 20:00:00'), +(54, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-23 05:00:00'), +(55, 13, 3, 'Answered a customer support conversation.', '2026-01-23 14:00:00'), +(56, 14, 3, 'Updated a product detail for the catalogue.', '2026-01-23 23:00:00'), +(57, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-24 08:00:00'), +(58, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-24 17:00:00'), +(59, 3, 1, 'Updated a pet availability record.', '2026-01-25 02:00:00'), +(60, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-25 11:00:00'), +(61, 5, 1, 'Checked a pending adoption record.', '2026-01-25 20:00:00'), +(62, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-26 05:00:00'), +(63, 7, 2, 'Answered a customer support conversation.', '2026-01-26 14:00:00'), +(64, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-26 23:00:00'), +(65, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-27 08:00:00'), +(66, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-27 17:00:00'), +(67, 11, 3, 'Updated a pet availability record.', '2026-01-28 02:00:00'), +(68, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-28 11:00:00'), +(69, 13, 3, 'Checked a pending adoption record.', '2026-01-28 20:00:00'), +(70, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-29 05:00:00'), +(71, 1, 1, 'Answered a customer support conversation.', '2026-01-29 14:00:00'), +(72, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-29 23:00:00'), +(73, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-30 08:00:00'), +(74, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-30 17:00:00'), +(75, 5, 1, 'Updated a pet availability record.', '2026-01-31 02:00:00'), +(76, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-31 11:00:00'), +(77, 7, 2, 'Checked a pending adoption record.', '2026-01-31 20:00:00'), +(78, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-01 05:00:00'), +(79, 9, 2, 'Answered a customer support conversation.', '2026-02-01 14:00:00'), +(80, 10, 2, 'Updated a product detail for the catalogue.', '2026-02-01 23:00:00'), +(81, 11, 3, 'Reviewed store inventory adjustments.', '2026-02-02 08:00:00'), +(82, 12, 3, 'Approved a purchase transaction at the register.', '2026-02-02 17:00:00'), +(83, 13, 3, 'Updated a pet availability record.', '2026-02-03 02:00:00'), +(84, 14, 3, 'Completed a grooming appointment handoff.', '2026-02-03 11:00:00'), +(85, 1, 1, 'Checked a pending adoption record.', '2026-02-03 20:00:00'), +(86, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-04 05:00:00'), +(87, 3, 1, 'Answered a customer support conversation.', '2026-02-04 14:00:00'), +(88, 4, 1, 'Updated a product detail for the catalogue.', '2026-02-04 23:00:00'), +(89, 5, 1, 'Reviewed store inventory adjustments.', '2026-02-05 08:00:00'), +(90, 6, 1, 'Approved a purchase transaction at the register.', '2026-02-05 17:00:00'), +(91, 7, 2, 'Updated a pet availability record.', '2026-02-06 02:00:00'), +(92, 8, 2, 'Completed a grooming appointment handoff.', '2026-02-06 11:00:00'), +(93, 9, 2, 'Checked a pending adoption record.', '2026-02-06 20:00:00'), +(94, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-07 05:00:00'), +(95, 11, 3, 'Answered a customer support conversation.', '2026-02-07 14:00:00'), +(96, 12, 3, 'Updated a product detail for the catalogue.', '2026-02-07 23:00:00'), +(97, 13, 3, 'Reviewed store inventory adjustments.', '2026-02-08 08:00:00'), +(98, 14, 3, 'Approved a purchase transaction at the register.', '2026-02-08 17:00:00'), +(99, 1, 1, 'Updated a pet availability record.', '2026-02-09 02:00:00'), +(100, 2, 2, 'Completed a grooming appointment handoff.', '2026-02-09 11:00:00'), +(101, 3, 1, 'Checked a pending adoption record.', '2026-02-09 20:00:00'), +(102, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-10 05:00:00'), +(103, 5, 1, 'Answered a customer support conversation.', '2026-02-10 14:00:00'), +(104, 6, 1, 'Updated a product detail for the catalogue.', '2026-02-10 23:00:00'), +(105, 7, 2, 'Reviewed store inventory adjustments.', '2026-02-11 08:00:00'), +(106, 8, 2, 'Approved a purchase transaction at the register.', '2026-02-11 17:00:00'), +(107, 9, 2, 'Updated a pet availability record.', '2026-02-12 02:00:00'), +(108, 10, 2, 'Completed a grooming appointment handoff.', '2026-02-12 11:00:00'), +(109, 11, 3, 'Checked a pending adoption record.', '2026-02-12 20:00:00'), +(110, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-02-13 05:00:00'), +(111, 13, 3, 'Answered a customer support conversation.', '2026-02-13 14:00:00'), +(112, 14, 3, 'Updated a product detail for the catalogue.', '2026-02-13 23:00:00'), +(113, 1, 1, 'Reviewed store inventory adjustments.', '2026-02-14 08:00:00'), +(114, 2, 2, 'Approved a purchase transaction at the register.', '2026-02-14 17:00:00'), +(115, 3, 1, 'Updated a pet availability record.', '2026-02-15 02:00:00'), +(116, 4, 1, 'Completed a grooming appointment handoff.', '2026-02-15 11:00:00'), +(117, 5, 1, 'Checked a pending adoption record.', '2026-02-15 20:00:00'), +(118, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-16 05:00:00'), +(119, 7, 2, 'Answered a customer support conversation.', '2026-02-16 14:00:00'), +(120, 8, 2, 'Updated a product detail for the catalogue.', '2026-02-16 23:00:00'); From 24b11e4152e7e195555f76330b2cc7f1fc9f7650 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 19:47:18 -0600 Subject: [PATCH 080/137] switch to target DB config --- backend/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index c9ba26d3..98aac596 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: max-request-size: 5MB datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3307/Petstoredb_target?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} username: ${SPRING_DATASOURCE_USERNAME:petshop} password: ${SPRING_DATASOURCE_PASSWORD:petshop} driver-class-name: com.mysql.cj.jdbc.Driver @@ -20,7 +20,7 @@ spring: jpa: hibernate: - ddl-auto: validate + ddl-auto: none naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: ${JPA_SHOW_SQL:false} From 824ed7e5eb830f30b83a6e8a4674970d261a9657 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 19:49:38 -0600 Subject: [PATCH 081/137] expand User entity fields --- .../petshop/backend/dto/user/UserRequest.java | 67 +++++++++++---- .../backend/dto/user/UserResponse.java | 74 +++++++++++------ .../java/com/petshop/backend/entity/User.java | 83 ++++++++++++++----- .../petshop/backend/service/UserService.java | 38 +++++++-- 4 files changed, 191 insertions(+), 71 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java index 09a9036d..a6eb61cb 100644 --- a/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserRequest.java @@ -8,14 +8,20 @@ import jakarta.validation.constraints.Size; import java.util.Objects; public class UserRequest { - @NotBlank(message = "Username is required") @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") private String username; @Size(min = 6, message = "Password must be at least 6 characters") private String password; - @NotBlank(message = "Full name is required") + @NotBlank(message = "First name is required") + @Size(max = 50) + private String firstName; + + @NotBlank(message = "Last name is required") + @Size(max = 50) + private String lastName; + private String fullName; @Email(message = "Invalid email format") @@ -27,6 +33,10 @@ public class UserRequest { @NotNull(message = "Role is required") private User.Role role; + private String staffRole; + + private Long primaryStoreId; + private Boolean active = true; public String getUsername() { @@ -45,6 +55,22 @@ public class UserRequest { this.password = password; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return fullName; } @@ -77,6 +103,22 @@ public class UserRequest { this.role = role; } + public String getStaffRole() { + return staffRole; + } + + public void setStaffRole(String staffRole) { + this.staffRole = staffRole; + } + + public Long getPrimaryStoreId() { + return primaryStoreId; + } + + public void setPrimaryStoreId(Long primaryStoreId) { + this.primaryStoreId = primaryStoreId; + } + public Boolean getActive() { return active; } @@ -91,29 +133,20 @@ public class UserRequest { if (o == null || getClass() != o.getClass()) return false; UserRequest that = (UserRequest) o; return Objects.equals(username, that.username) && - Objects.equals(password, that.password) && - Objects.equals(fullName, that.fullName) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email) && - Objects.equals(phone, that.phone) && - role == that.role && - Objects.equals(active, that.active); + role == that.role; } @Override public int hashCode() { - return Objects.hash(username, password, fullName, email, phone, role, active); + return Objects.hash(username, firstName, lastName, email, role); } @Override public String toString() { - return "UserRequest{" + - "username='" + username + '\'' + - ", password='" + password + '\'' + - ", fullName='" + fullName + '\'' + - ", email='" + email + '\'' + - ", phone='" + phone + '\'' + - ", role=" + role + - ", active=" + active + - '}'; + return "UserRequest{username='" + username + "', firstName='" + firstName + + "', lastName='" + lastName + "', role=" + role + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java index 9d7167c2..6c4d15b5 100644 --- a/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/user/UserResponse.java @@ -6,10 +6,15 @@ import java.util.Objects; public class UserResponse { private Long id; private String username; + private String firstName; + private String lastName; private String fullName; private String email; private String phone; private String role; + private String staffRole; + private Long primaryStoreId; + private Integer loyaltyPoints; private Boolean active; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -17,18 +22,6 @@ public class UserResponse { public UserResponse() { } - public UserResponse(Long id, String username, String fullName, String email, String phone, String role, Boolean active, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.username = username; - this.fullName = fullName; - this.email = email; - this.phone = phone; - this.role = role; - this.active = active; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getId() { return id; } @@ -45,6 +38,22 @@ public class UserResponse { this.username = username; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return fullName; } @@ -77,6 +86,30 @@ public class UserResponse { this.role = role; } + public String getStaffRole() { + return staffRole; + } + + public void setStaffRole(String staffRole) { + this.staffRole = staffRole; + } + + public Long getPrimaryStoreId() { + return primaryStoreId; + } + + public void setPrimaryStoreId(Long primaryStoreId) { + this.primaryStoreId = primaryStoreId; + } + + public Integer getLoyaltyPoints() { + return loyaltyPoints; + } + + public void setLoyaltyPoints(Integer loyaltyPoints) { + this.loyaltyPoints = loyaltyPoints; + } + public Boolean getActive() { return active; } @@ -106,26 +139,17 @@ public class UserResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserResponse that = (UserResponse) o; - return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(fullName, that.fullName) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone) && Objects.equals(role, that.role) && Objects.equals(active, that.active) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(id, that.id); } @Override public int hashCode() { - return Objects.hash(id, username, fullName, email, phone, role, active, createdAt, updatedAt); + return Objects.hash(id); } @Override public String toString() { - return "UserResponse{" + - "id=" + id + - ", username='" + username + '\'' + - ", fullName='" + fullName + '\'' + - ", email='" + email + '\'' + - ", phone='" + phone + '\'' + - ", role='" + role + '\'' + - ", active=" + active + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; + return "UserResponse{id=" + id + ", username='" + username + "', firstName='" + firstName + + "', lastName='" + lastName + "', role='" + role + "', active=" + active + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/entity/User.java b/backend/src/main/java/com/petshop/backend/entity/User.java index d13e2e68..97b8dfc5 100644 --- a/backend/src/main/java/com/petshop/backend/entity/User.java +++ b/backend/src/main/java/com/petshop/backend/entity/User.java @@ -16,15 +16,21 @@ public class User { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, unique = true, length = 50) + @Column(unique = true, length = 50) private String username; - @Column(nullable = false) + @Column private String password; @Column(unique = true, length = 100) private String email; + @Column(nullable = false, length = 50) + private String firstName; + + @Column(nullable = false, length = 50) + private String lastName; + @Column(length = 100) private String fullName; @@ -38,6 +44,16 @@ public class User { @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") private Role role; + @Column(length = 50) + private String staffRole; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "primaryStoreId") + private StoreLocation primaryStore; + + @Column(nullable = false) + private Integer loyaltyPoints = 0; + @Column(nullable = false) private Boolean active = true; @@ -59,21 +75,6 @@ public class User { public User() { } - public User(Long id, String username, String password, String email, String fullName, String phone, String avatarUrl, Role role, Boolean active, Integer tokenVersion, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.username = username; - this.password = password; - this.email = email; - this.fullName = fullName; - this.phone = phone; - this.avatarUrl = avatarUrl; - this.role = role; - this.active = active; - this.tokenVersion = tokenVersion; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getId() { return id; } @@ -106,6 +107,22 @@ public class User { this.email = email; } + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getFullName() { return fullName; } @@ -138,6 +155,30 @@ public class User { this.role = role; } + public String getStaffRole() { + return staffRole; + } + + public void setStaffRole(String staffRole) { + this.staffRole = staffRole; + } + + public StoreLocation getPrimaryStore() { + return primaryStore; + } + + public void setPrimaryStore(StoreLocation primaryStore) { + this.primaryStore = primaryStore; + } + + public Integer getLoyaltyPoints() { + return loyaltyPoints; + } + + public void setLoyaltyPoints(Integer loyaltyPoints) { + this.loyaltyPoints = loyaltyPoints; + } + public Boolean getActive() { return active; } @@ -188,16 +229,12 @@ public class User { return "User{" + "id=" + id + ", username='" + username + '\'' + - ", password='" + password + '\'' + ", email='" + email + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + ", fullName='" + fullName + '\'' + - ", phone='" + phone + '\'' + - ", avatarUrl='" + avatarUrl + '\'' + ", role=" + role + ", active=" + active + - ", tokenVersion=" + tokenVersion + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index 3c219172..e3a17c5c 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -3,8 +3,10 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.user.UserRequest; import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -14,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; import java.util.Locale; +import java.util.Objects; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; @@ -24,11 +27,13 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final UserBusinessLinkageService userBusinessLinkageService; + private final StoreRepository storeRepository; - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, StoreRepository storeRepository) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.userBusinessLinkageService = userBusinessLinkageService; + this.storeRepository = storeRepository; } public Page getAllUsers(String query, String role, Pageable pageable) { @@ -56,12 +61,18 @@ public class UserService { @Transactional public UserResponse createUser(UserRequest request) { User user = new User(); - user.setUsername(request.getUsername()); - user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setUsername(trimToNull(request.getUsername())); + if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { + user.setPassword(passwordEncoder.encode(request.getPassword())); + } + user.setFirstName(request.getFirstName()); + user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); user.setPhone(trimToNull(request.getPhone())); user.setRole(request.getRole()); + user.setStaffRole(trimToNull(request.getStaffRole())); + user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); validateUniquePhone(user.getPhone(), null); @@ -79,23 +90,27 @@ public class UserService { .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id)); boolean invalidateToken = - !user.getUsername().equals(request.getUsername()) + !Objects.equals(user.getUsername(), request.getUsername()) || user.getRole() != request.getRole() || !user.getActive().equals(request.getActive() != null ? request.getActive() : true); - user.setUsername(request.getUsername()); + user.setUsername(trimToNull(request.getUsername())); if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); invalidateToken = true; } + user.setFirstName(request.getFirstName()); + user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); String phone = trimToNull(request.getPhone()); - if (!java.util.Objects.equals(user.getPhone(), phone)) { + if (!Objects.equals(user.getPhone(), phone)) { validateUniquePhone(phone, user.getId()); } user.setPhone(phone); user.setRole(request.getRole()); + user.setStaffRole(trimToNull(request.getStaffRole())); + user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); if (invalidateToken) { user.setTokenVersion(user.getTokenVersion() + 1); @@ -123,16 +138,27 @@ public class UserService { UserResponse response = new UserResponse(); response.setId(user.getId()); response.setUsername(user.getUsername()); + response.setFirstName(user.getFirstName()); + response.setLastName(user.getLastName()); response.setFullName(user.getFullName()); response.setEmail(user.getEmail()); response.setPhone(user.getPhone()); response.setRole(user.getRole().toString()); + response.setStaffRole(user.getStaffRole()); + response.setPrimaryStoreId(user.getPrimaryStore() != null ? user.getPrimaryStore().getStoreId() : null); + response.setLoyaltyPoints(user.getLoyaltyPoints()); response.setActive(user.getActive()); response.setCreatedAt(user.getCreatedAt()); response.setUpdatedAt(user.getUpdatedAt()); return response; } + private StoreLocation resolveStore(Long storeId) { + if (storeId == null) return null; + return storeRepository.findById(storeId) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); + } + private void validateUniquePhone(String phone, Long currentUserId) { if (phone == null || phone.isBlank()) { return; From 2360dc2419474ac28858d0c5a9f2fac52856d189 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:17:27 -0600 Subject: [PATCH 082/137] merge customer/employee into users --- .../backend/config/DataInitializer.java | 59 +++--- ...calAppointmentCustomerSeedInitializer.java | 34 ---- .../controller/AdoptionController.java | 15 +- .../controller/AppointmentController.java | 19 +- .../backend/controller/AuthController.java | 83 ++------ .../backend/controller/ChatController.java | 5 +- .../controller/CustomerController.java | 34 ++-- .../controller/CustomerPetController.java | 118 ----------- .../controller/DropdownController.java | 63 +----- .../controller/EmployeeController.java | 35 ++-- .../backend/controller/RefundController.java | 19 +- .../dto/appointment/AppointmentRequest.java | 14 +- .../dto/appointment/AppointmentResponse.java | 20 -- .../backend/dto/customer/CustomerRequest.java | 64 ------ .../dto/customer/CustomerResponse.java | 98 ---------- .../dto/customerpet/CustomerPetRequest.java | 66 ------- .../dto/customerpet/CustomerPetResponse.java | 123 ------------ .../backend/dto/employee/EmployeeRequest.java | 51 ----- .../dto/employee/EmployeeResponse.java | 43 ---- .../petshop/backend/entity/ActivityLog.java | 18 +- .../com/petshop/backend/entity/Adoption.java | 31 +-- .../petshop/backend/entity/Appointment.java | 43 +--- .../com/petshop/backend/entity/Customer.java | 132 ------------- .../petshop/backend/entity/CustomerPet.java | 137 ------------- .../com/petshop/backend/entity/Employee.java | 158 --------------- .../petshop/backend/entity/EmployeeStore.java | 117 ----------- .../java/com/petshop/backend/entity/Pet.java | 61 ++---- .../java/com/petshop/backend/entity/Sale.java | 34 +--- .../repository/AdoptionRepository.java | 4 +- .../repository/AppointmentRepository.java | 12 +- .../repository/CustomerPetRepository.java | 18 -- .../repository/CustomerRepository.java | 29 --- .../repository/EmployeeRepository.java | 30 --- .../repository/EmployeeStoreRepository.java | 21 -- .../backend/repository/PetRepository.java | 8 +- .../backend/repository/UserRepository.java | 13 +- .../backend/service/AdoptionService.java | 47 ++--- .../backend/service/AnalyticsService.java | 15 +- .../backend/service/AppointmentService.java | 129 +++--------- .../backend/service/ChatRealtimeService.java | 14 +- .../petshop/backend/service/ChatService.java | 20 +- .../backend/service/CustomerPetService.java | 163 ---------------- .../backend/service/CustomerService.java | 158 --------------- .../backend/service/EmployeeService.java | 183 ------------------ .../petshop/backend/service/PetService.java | 35 ++-- .../backend/service/RefundService.java | 4 +- .../petshop/backend/service/SaleService.java | 26 +-- .../service/StoreAssignmentService.java | 34 ---- .../service/UserBusinessLinkageService.java | 152 --------------- .../petshop/backend/service/UserService.java | 7 +- .../backend/util/AuthenticationHelper.java | 16 -- 51 files changed, 286 insertions(+), 2546 deletions(-) delete mode 100644 backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java delete mode 100644 backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java delete mode 100644 backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java delete mode 100644 backend/src/main/java/com/petshop/backend/entity/Customer.java delete mode 100644 backend/src/main/java/com/petshop/backend/entity/CustomerPet.java delete mode 100644 backend/src/main/java/com/petshop/backend/entity/Employee.java delete mode 100644 backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java delete mode 100644 backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java delete mode 100644 backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java delete mode 100644 backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java delete mode 100644 backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java delete mode 100644 backend/src/main/java/com/petshop/backend/service/CustomerPetService.java delete mode 100644 backend/src/main/java/com/petshop/backend/service/CustomerService.java delete mode 100644 backend/src/main/java/com/petshop/backend/service/EmployeeService.java delete mode 100644 backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java delete mode 100644 backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java diff --git a/backend/src/main/java/com/petshop/backend/config/DataInitializer.java b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java index 4a8c7470..9809ddc6 100644 --- a/backend/src/main/java/com/petshop/backend/config/DataInitializer.java +++ b/backend/src/main/java/com/petshop/backend/config/DataInitializer.java @@ -2,8 +2,6 @@ package com.petshop.backend.config; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; -import com.petshop.backend.service.StoreAssignmentService; -import com.petshop.backend.service.UserBusinessLinkageService; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -13,14 +11,10 @@ public class DataInitializer implements CommandLineRunner { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; - private final StoreAssignmentService storeAssignmentService; - public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, StoreAssignmentService storeAssignmentService) { + public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; - this.storeAssignmentService = storeAssignmentService; } @Override @@ -34,16 +28,25 @@ public class DataInitializer implements CommandLineRunner { admin.setUsername("admin"); admin.setPassword(passwordEncoder.encode("admin123")); admin.setEmail("admin@petshop.com"); + admin.setFirstName("Admin"); + admin.setLastName("User"); admin.setFullName("Admin User"); admin.setPhone("000-000-1000"); admin.setRole(User.Role.ADMIN); admin.setActive(true); - admin = userRepository.save(admin); + userRepository.save(admin); System.out.println("Admin user created successfully"); } else { System.out.println("Admin user already exists"); - // Normalize missing fields if needed boolean updated = false; + if (admin.getFirstName() == null || admin.getFirstName().isEmpty()) { + admin.setFirstName("Admin"); + updated = true; + } + if (admin.getLastName() == null || admin.getLastName().isEmpty()) { + admin.setLastName("User"); + updated = true; + } if (admin.getFullName() == null || admin.getFullName().isEmpty()) { admin.setFullName("Admin User"); updated = true; @@ -65,12 +68,10 @@ public class DataInitializer implements CommandLineRunner { updated = true; } if (updated) { - admin = userRepository.save(admin); + userRepository.save(admin); System.out.println("Admin user normalized"); } } - // Ensure linked employee - storeAssignmentService.assignStoreIfMissing(userBusinessLinkageService.ensureLinkedEmployee(admin), 1L); User staff = userRepository.findByUsername("staff").orElse(null); if (staff == null) { @@ -79,16 +80,25 @@ public class DataInitializer implements CommandLineRunner { staff.setUsername("staff"); staff.setPassword(passwordEncoder.encode("staff123")); staff.setEmail("staff@petshop.com"); + staff.setFirstName("Staff"); + staff.setLastName("User"); staff.setFullName("Staff User"); staff.setPhone("000-000-1001"); staff.setRole(User.Role.STAFF); staff.setActive(true); - staff = userRepository.save(staff); + userRepository.save(staff); System.out.println("Staff user created successfully"); } else { System.out.println("Staff user already exists"); - // Normalize missing fields if needed boolean updated = false; + if (staff.getFirstName() == null || staff.getFirstName().isEmpty()) { + staff.setFirstName("Staff"); + updated = true; + } + if (staff.getLastName() == null || staff.getLastName().isEmpty()) { + staff.setLastName("User"); + updated = true; + } if (staff.getFullName() == null || staff.getFullName().isEmpty()) { staff.setFullName("Staff User"); updated = true; @@ -110,12 +120,10 @@ public class DataInitializer implements CommandLineRunner { updated = true; } if (updated) { - staff = userRepository.save(staff); + userRepository.save(staff); System.out.println("Staff user normalized"); } } - // Ensure linked employee - storeAssignmentService.assignStoreIfMissing(userBusinessLinkageService.ensureLinkedEmployee(staff), 1L); User customer = userRepository.findByUsername("customer").orElse(null); if (customer == null) { @@ -124,16 +132,25 @@ public class DataInitializer implements CommandLineRunner { customer.setUsername("customer"); customer.setPassword(passwordEncoder.encode("customer123")); customer.setEmail("customer@petshop.com"); + customer.setFirstName("Test"); + customer.setLastName("Customer"); customer.setFullName("Test Customer"); customer.setPhone("000-000-1002"); customer.setRole(User.Role.CUSTOMER); customer.setActive(true); - customer = userRepository.save(customer); + userRepository.save(customer); System.out.println("Customer user created successfully"); } else { System.out.println("Customer user already exists"); - // Normalize missing fields if needed boolean updated = false; + if (customer.getFirstName() == null || customer.getFirstName().isEmpty()) { + customer.setFirstName("Test"); + updated = true; + } + if (customer.getLastName() == null || customer.getLastName().isEmpty()) { + customer.setLastName("Customer"); + updated = true; + } if (customer.getFullName() == null || customer.getFullName().isEmpty()) { customer.setFullName("Test Customer"); updated = true; @@ -155,12 +172,10 @@ public class DataInitializer implements CommandLineRunner { updated = true; } if (updated) { - customer = userRepository.save(customer); + userRepository.save(customer); System.out.println("Customer user normalized"); } } - // Ensure linked customer - userBusinessLinkageService.ensureLinkedCustomer(customer); System.out.println("==== DataInitializer: Completed ===="); } diff --git a/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java b/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java deleted file mode 100644 index 36b78fb4..00000000 --- a/backend/src/main/java/com/petshop/backend/config/LocalAppointmentCustomerSeedInitializer.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.petshop.backend.config; - -import com.petshop.backend.repository.CustomerPetRepository; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.annotation.Profile; -import org.springframework.core.io.ClassPathResource; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; - -@Component -@Profile("local") -public class LocalAppointmentCustomerSeedInitializer implements CommandLineRunner { - - private final DataSource dataSource; - private final CustomerPetRepository customerPetRepository; - - public LocalAppointmentCustomerSeedInitializer(DataSource dataSource, CustomerPetRepository customerPetRepository) { - this.dataSource = dataSource; - this.customerPetRepository = customerPetRepository; - } - - @Override - public void run(String... args) { - if (customerPetRepository.count() > 0) { - return; - } - - ResourceDatabasePopulator populator = new ResourceDatabasePopulator(false, false, "UTF-8", - new ClassPathResource("dev/seed_demo_customer_pets.sql")); - populator.execute(dataSource); - } -} diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index 41a2e815..bcb61db4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -3,8 +3,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.adoption.AdoptionRequest; import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AdoptionService; import com.petshop.backend.util.AuthenticationHelper; @@ -24,12 +23,10 @@ public class AdoptionController { private final AdoptionService adoptionService; private final UserRepository userRepository; - private final CustomerRepository customerRepository; - public AdoptionController(AdoptionService adoptionService, UserRepository userRepository, CustomerRepository customerRepository) { + public AdoptionController(AdoptionService adoptionService, UserRepository userRepository) { this.adoptionService = adoptionService; this.userRepository = userRepository; - this.customerRepository = customerRepository; } @GetMapping @@ -45,8 +42,8 @@ public class AdoptionController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId)); @@ -63,8 +60,8 @@ public class AdoptionController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index 35246e05..cf04f041 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -3,8 +3,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.appointment.AppointmentRequest; import com.petshop.backend.dto.appointment.AppointmentResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AppointmentService; import com.petshop.backend.util.AuthenticationHelper; @@ -27,12 +26,10 @@ public class AppointmentController { private final AppointmentService appointmentService; private final UserRepository userRepository; - private final CustomerRepository customerRepository; - public AppointmentController(AppointmentService appointmentService, UserRepository userRepository, CustomerRepository customerRepository) { + public AppointmentController(AppointmentService appointmentService, UserRepository userRepository) { this.appointmentService = appointmentService; this.userRepository = userRepository; - this.customerRepository = customerRepository; } @GetMapping @@ -48,8 +45,8 @@ public class AppointmentController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId)); @@ -66,8 +63,8 @@ public class AppointmentController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); @@ -83,8 +80,8 @@ public class AppointmentController { .orElse(null); if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - if (!request.getCustomerId().equals(customer.getCustomerId())) { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + if (!request.getCustomerId().equals(user.getId())) { throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); } } diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 106ea66f..641c7a6b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -7,15 +7,11 @@ import com.petshop.backend.dto.auth.ProfileUpdateRequest; import com.petshop.backend.dto.auth.RegisterRequest; import com.petshop.backend.dto.auth.RegisterResponse; import com.petshop.backend.dto.auth.UserInfoResponse; -import com.petshop.backend.entity.EmployeeStore; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; -import com.petshop.backend.repository.EmployeeStoreRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.JwtUtil; import com.petshop.backend.service.AvatarStorageService; -import com.petshop.backend.service.UserBusinessLinkageService; import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; import org.springframework.core.io.Resource; @@ -44,22 +40,14 @@ public class AuthController { private final UserRepository userRepository; private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; - private final EmployeeRepository employeeRepository; - private final EmployeeStoreRepository employeeStoreRepository; private final AvatarStorageService avatarStorageService; - private final CustomerRepository customerRepository; - public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService, CustomerRepository customerRepository) { + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.jwtUtil = jwtUtil; this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; - this.employeeRepository = employeeRepository; - this.employeeStoreRepository = employeeStoreRepository; this.avatarStorageService = avatarStorageService; - this.customerRepository = customerRepository; } @PostMapping("/register") @@ -94,9 +82,6 @@ public class AuthController { User savedUser = userRepository.save(user); - // Create or link customer record - userBusinessLinkageService.ensureLinkedCustomer(savedUser); - String token = jwtUtil.generateToken(savedUser); return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse( @@ -148,22 +133,7 @@ public class AuthController { @GetMapping("/me") public ResponseEntity getCurrentUser() { User user = getAuthenticatedUser(); - - EmployeeStore employeeStore = resolveEmployeeStore(user); - Long customerId = resolveCustomerId(user); - - return ResponseEntity.ok(new UserInfoResponse( - user.getId(), - user.getUsername(), - user.getEmail(), - user.getFullName(), - user.getPhone(), - avatarStorageService.toOwnerAvatarUrl(user), - user.getRole().name(), - customerId, - employeeStore != null ? employeeStore.getStore().getStoreId() : null, - employeeStore != null ? employeeStore.getStore().getStoreName() : null - )); + return ResponseEntity.ok(toUserInfoResponse(user)); } @PutMapping("/me") @@ -218,39 +188,24 @@ public class AuthController { } User updatedUser = userRepository.save(user); - userBusinessLinkageService.syncLinkedRecords(updatedUser); + return ResponseEntity.ok(toUserInfoResponse(updatedUser)); + } - EmployeeStore employeeStore = resolveEmployeeStore(updatedUser); - Long customerId = resolveCustomerId(updatedUser); - - return ResponseEntity.ok(new UserInfoResponse( - updatedUser.getId(), - updatedUser.getUsername(), - updatedUser.getEmail(), - updatedUser.getFullName(), - updatedUser.getPhone(), - avatarStorageService.toOwnerAvatarUrl(updatedUser), - updatedUser.getRole().name(), + private UserInfoResponse toUserInfoResponse(User user) { + StoreLocation primaryStore = user.getPrimaryStore(); + Long customerId = user.getRole() == User.Role.CUSTOMER ? user.getId() : null; + return new UserInfoResponse( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getFullName(), + user.getPhone(), + avatarStorageService.toOwnerAvatarUrl(user), + user.getRole().name(), customerId, - employeeStore != null ? employeeStore.getStore().getStoreId() : null, - employeeStore != null ? employeeStore.getStore().getStoreName() : null - )); - } - - private EmployeeStore resolveEmployeeStore(User user) { - if (user.getRole() == User.Role.CUSTOMER) { - return null; - } - - return employeeRepository.findByUserId(user.getId()) - .flatMap(employee -> employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId())) - .orElse(null); - } - - private Long resolveCustomerId(User user) { - return customerRepository.findByUserId(user.getId()) - .map(c -> c.getCustomerId()) - .orElse(null); + primaryStore != null ? primaryStore.getStoreId() : null, + primaryStore != null ? primaryStore.getStoreName() : null + ); } private String trimToNull(String value) { diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index 7320cdb9..e56e353f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -6,7 +6,6 @@ import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.ChatRealtimeService; import com.petshop.backend.service.ChatService; @@ -27,13 +26,11 @@ public class ChatController { private final ChatService chatService; private final ChatRealtimeService chatRealtimeService; private final UserRepository userRepository; - private final CustomerRepository customerRepository; - public ChatController(ChatService chatService, ChatRealtimeService chatRealtimeService, UserRepository userRepository, CustomerRepository customerRepository) { + public ChatController(ChatService chatService, ChatRealtimeService chatRealtimeService, UserRepository userRepository) { this.chatService = chatService; this.chatRealtimeService = chatRealtimeService; this.userRepository = userRepository; - this.customerRepository = customerRepository; } private User getCurrentUser() { diff --git a/backend/src/main/java/com/petshop/backend/controller/CustomerController.java b/backend/src/main/java/com/petshop/backend/controller/CustomerController.java index f3ab880e..4f17dd4f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/CustomerController.java +++ b/backend/src/main/java/com/petshop/backend/controller/CustomerController.java @@ -1,9 +1,9 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.BulkDeleteRequest; -import com.petshop.backend.dto.customer.CustomerRequest; -import com.petshop.backend.dto.customer.CustomerResponse; -import com.petshop.backend.service.CustomerService; +import com.petshop.backend.dto.user.UserRequest; +import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.service.UserService; import jakarta.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -17,45 +17,45 @@ import org.springframework.web.bind.annotation.*; @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public class CustomerController { - private final CustomerService customerService; + private final UserService userService; - public CustomerController(CustomerService customerService) { - this.customerService = customerService; + public CustomerController(UserService userService) { + this.userService = userService; } @GetMapping - public ResponseEntity> getAllCustomers( + public ResponseEntity> getAllCustomers( @RequestParam(required = false) String q, Pageable pageable) { - return ResponseEntity.ok(customerService.getAllCustomers(q, pageable)); + return ResponseEntity.ok(userService.getAllUsers(q, "CUSTOMER", pageable)); } @GetMapping("/{id}") - public ResponseEntity getCustomerById(@PathVariable Long id) { - return ResponseEntity.ok(customerService.getCustomerById(id)); + public ResponseEntity getCustomerById(@PathVariable Long id) { + return ResponseEntity.ok(userService.getUserById(id)); } @PostMapping - public ResponseEntity createCustomer(@Valid @RequestBody CustomerRequest request) { - return ResponseEntity.status(HttpStatus.CREATED).body(customerService.createCustomer(request)); + public ResponseEntity createCustomer(@Valid @RequestBody UserRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request)); } @PutMapping("/{id}") - public ResponseEntity updateCustomer( + public ResponseEntity updateCustomer( @PathVariable Long id, - @Valid @RequestBody CustomerRequest request) { - return ResponseEntity.ok(customerService.updateCustomer(id, request)); + @Valid @RequestBody UserRequest request) { + return ResponseEntity.ok(userService.updateUser(id, request)); } @DeleteMapping("/{id}") public ResponseEntity deleteCustomer(@PathVariable Long id) { - customerService.deleteCustomer(id); + userService.deleteUser(id); return ResponseEntity.noContent().build(); } @PostMapping("/bulk-delete") public ResponseEntity bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) { - customerService.bulkDeleteCustomers(request); + userService.bulkDeleteUsers(request); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java b/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java deleted file mode 100644 index 4fd6648b..00000000 --- a/backend/src/main/java/com/petshop/backend/controller/CustomerPetController.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.petshop.backend.controller; - -import com.petshop.backend.dto.customerpet.CustomerPetRequest; -import com.petshop.backend.dto.customerpet.CustomerPetResponse; -import com.petshop.backend.service.CatalogImageStorageService; -import com.petshop.backend.service.CustomerPetService; -import com.petshop.backend.entity.CustomerPet; -import com.petshop.backend.repository.CustomerPetRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.UserRepository; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.util.AuthenticationHelper; -import jakarta.validation.Valid; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/my-pets") -@PreAuthorize("hasRole('CUSTOMER')") -public class CustomerPetController { - - private final CustomerPetService customerPetService; - private final CustomerPetRepository customerPetRepository; - private final CustomerRepository customerRepository; - private final UserRepository userRepository; - private final CatalogImageStorageService catalogImageStorageService; - - public CustomerPetController(CustomerPetService customerPetService, - CustomerPetRepository customerPetRepository, - CustomerRepository customerRepository, - UserRepository userRepository, - CatalogImageStorageService catalogImageStorageService) { - this.customerPetService = customerPetService; - this.customerPetRepository = customerPetRepository; - this.customerRepository = customerRepository; - this.userRepository = userRepository; - this.catalogImageStorageService = catalogImageStorageService; - } - - @GetMapping - public ResponseEntity> getMyPets() { - - return ResponseEntity.ok(customerPetService.getMyPets()); - } - - @PostMapping - public ResponseEntity createPet(@Valid @RequestBody CustomerPetRequest request) { - - return ResponseEntity.status(HttpStatus.CREATED).body(customerPetService.createPet(request)); - } - - @PutMapping("/{id}") - public ResponseEntity updatePet(@PathVariable Long id, @Valid @RequestBody CustomerPetRequest request) { - - return ResponseEntity.ok(customerPetService.updatePet(id, request)); - } - - @DeleteMapping("/{id}") - public ResponseEntity deletePet(@PathVariable Long id) { - customerPetService.deletePet(id); - - return ResponseEntity.noContent().build(); - } - - @PostMapping("/{id}/image") - public ResponseEntity uploadImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { - try { - - return ResponseEntity.ok(customerPetService.uploadImage(id, image)); - } - - catch (IllegalArgumentException ex) { - Map error = new HashMap<>(); - error.put("message", ex.getMessage()); - - return ResponseEntity.badRequest().body(error); - } - - catch (IOException ex) { - Map error = new HashMap<>(); - error.put("message", "Failed to upload image: " + ex.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); - } - } - - @GetMapping("/{id}/image") - public ResponseEntity getImage(@PathVariable Long id) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElse(null); - - if (pet == null || pet.getImageUrl() == null || pet.getImageUrl().isBlank()) { - - return ResponseEntity.notFound().build(); - } - - Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl()); - MediaType mediaType = catalogImageStorageService.resolveMediaType(resource); - - return ResponseEntity.ok().contentType(mediaType).body(resource); - } - - @DeleteMapping("/{id}/image") - public ResponseEntity deleteImage(@PathVariable Long id) { - - return ResponseEntity.ok(customerPetService.deleteImage(id)); - } -} diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index d0a69036..4d995cd4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -1,8 +1,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.DropdownOption; -import com.petshop.backend.entity.CustomerPet; -import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.User; import com.petshop.backend.repository.*; import org.springframework.http.ResponseEntity; @@ -20,31 +18,24 @@ import java.util.stream.Collectors; public class DropdownController { private final PetRepository petRepository; - private final CustomerRepository customerRepository; - private final CustomerPetRepository customerPetRepository; private final ServiceRepository serviceRepository; private final ProductRepository productRepository; private final CategoryRepository categoryRepository; private final StoreRepository storeRepository; private final SupplierRepository supplierRepository; - private final EmployeeStoreRepository employeeStoreRepository; private final UserRepository userRepository; - public DropdownController(PetRepository petRepository, CustomerRepository customerRepository, - CustomerPetRepository customerPetRepository, + public DropdownController(PetRepository petRepository, ServiceRepository serviceRepository, ProductRepository productRepository, CategoryRepository categoryRepository, StoreRepository storeRepository, - SupplierRepository supplierRepository, EmployeeStoreRepository employeeStoreRepository, + SupplierRepository supplierRepository, UserRepository userRepository) { this.petRepository = petRepository; - this.customerRepository = customerRepository; - this.customerPetRepository = customerPetRepository; this.serviceRepository = serviceRepository; this.productRepository = productRepository; this.categoryRepository = categoryRepository; this.storeRepository = storeRepository; this.supplierRepository = supplierRepository; - this.employeeStoreRepository = employeeStoreRepository; this.userRepository = userRepository; } @@ -71,8 +62,8 @@ public class DropdownController { @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getCustomers() { return ResponseEntity.ok( - customerRepository.findAll().stream() - .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) + userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() + .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) .collect(Collectors.toList()) ); } @@ -81,18 +72,8 @@ public class DropdownController { @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity> getAppointmentCustomers() { return ResponseEntity.ok( - customerRepository.findAllWithPets().stream() - .map(c -> new DropdownOption(c.getCustomerId(), c.getFirstName() + " " + c.getLastName())) - .collect(Collectors.toList()) - ); - } - - @GetMapping("/customers/{customerId}/pets") - @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") - public ResponseEntity> getCustomerPets(@PathVariable Long customerId) { - return ResponseEntity.ok( - customerPetRepository.findByCustomerCustomerIdOrderByPetNameAsc(customerId).stream() - .map(this::toCustomerPetOption) + userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() + .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) .collect(Collectors.toList()) ); } @@ -159,17 +140,15 @@ public class DropdownController { @GetMapping({"/stores/{storeId}/employees", "/employees"}) @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getStoreEmployees(@PathVariable(required = false) Long storeId) { - List employees; + List employees; if (storeId == null || storeId == 0) { - employees = employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc(); + employees = userRepository.findByRoleAndActiveTrue(User.Role.STAFF); } else { - employees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId); + employees = userRepository.findByPrimaryStoreStoreIdAndRoleAndActiveTrue(storeId, User.Role.STAFF); } return ResponseEntity.ok( employees.stream() - .filter(this::isAssignableEmployee) - .map(this::toEmployeeOption) - .distinct() + .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) .collect(Collectors.toList()) ); } @@ -183,26 +162,4 @@ public class DropdownController { .collect(Collectors.toList()) ); } - - private DropdownOption toCustomerPetOption(CustomerPet pet) { - String species = pet.getSpecies() == null || pet.getSpecies().isBlank() ? "Pet" : pet.getSpecies(); - String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " · " + pet.getBreed(); - return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")"); - } - - private DropdownOption toEmployeeOption(EmployeeStore employeeStore) { - var employee = employeeStore.getEmployee(); - return new DropdownOption(employee.getEmployeeId(), employee.getFirstName() + " " + employee.getLastName()); - } - - private boolean isAssignableEmployee(EmployeeStore employeeStore) { - Long userId = employeeStore.getEmployee().getUserId(); - if (userId == null) { - return false; - } - return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) - .filter(user -> Boolean.TRUE.equals(user.getActive())) - .isPresent(); - } } diff --git a/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java b/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java index 1c567623..2276e6ad 100644 --- a/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java +++ b/backend/src/main/java/com/petshop/backend/controller/EmployeeController.java @@ -1,8 +1,8 @@ package com.petshop.backend.controller; -import com.petshop.backend.dto.employee.EmployeeRequest; -import com.petshop.backend.dto.employee.EmployeeResponse; -import com.petshop.backend.service.EmployeeService; +import com.petshop.backend.dto.user.UserRequest; +import com.petshop.backend.dto.user.UserResponse; +import com.petshop.backend.service.UserService; import jakarta.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -15,35 +15,40 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/api/v1/employees") @PreAuthorize("hasRole('ADMIN')") public class EmployeeController { - private final EmployeeService employeeService; - public EmployeeController(EmployeeService employeeService) { - this.employeeService = employeeService; + private final UserService userService; + + public EmployeeController(UserService userService) { + this.userService = userService; } @GetMapping - public ResponseEntity> getAllEmployees(@RequestParam(required = false) String q, Pageable pageable) { - return ResponseEntity.ok(employeeService.getAllEmployees(q, pageable)); + public ResponseEntity> getAllEmployees( + @RequestParam(required = false) String q, + Pageable pageable) { + return ResponseEntity.ok(userService.getAllUsers(q, "STAFF", pageable)); } @GetMapping("/{id}") - public ResponseEntity getEmployeeById(@PathVariable Long id) { - return ResponseEntity.ok(employeeService.getEmployeeById(id)); + public ResponseEntity getEmployeeById(@PathVariable Long id) { + return ResponseEntity.ok(userService.getUserById(id)); } @PostMapping - public ResponseEntity createEmployee(@Valid @RequestBody EmployeeRequest request) { - return ResponseEntity.status(HttpStatus.CREATED).body(employeeService.createEmployee(request)); + public ResponseEntity createEmployee(@Valid @RequestBody UserRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request)); } @PutMapping("/{id}") - public ResponseEntity updateEmployee(@PathVariable Long id, @Valid @RequestBody EmployeeRequest request) { - return ResponseEntity.ok(employeeService.updateEmployee(id, request)); + public ResponseEntity updateEmployee( + @PathVariable Long id, + @Valid @RequestBody UserRequest request) { + return ResponseEntity.ok(userService.updateUser(id, request)); } @DeleteMapping("/{id}") public ResponseEntity deleteEmployee(@PathVariable Long id) { - employeeService.deleteEmployee(id); + userService.deleteUser(id); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index 6968b9c3..bd6f158b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -3,8 +3,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.refund.RefundRequest; import com.petshop.backend.dto.refund.RefundResponse; import com.petshop.backend.dto.refund.RefundUpdateRequest; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.repository.CustomerRepository; +import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.RefundService; import com.petshop.backend.util.AuthenticationHelper; @@ -26,12 +25,10 @@ public class RefundController { private final RefundService refundService; private final UserRepository userRepository; - private final CustomerRepository customerRepository; - public RefundController(RefundService refundService, UserRepository userRepository, CustomerRepository customerRepository) { + public RefundController(RefundService refundService, UserRepository userRepository) { this.refundService = refundService; this.userRepository = userRepository; - this.customerRepository = customerRepository; } @PostMapping @@ -46,8 +43,8 @@ public class RefundController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } RefundResponse refund = refundService.createRefund(request, customerId); @@ -70,8 +67,8 @@ public class RefundController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } List refunds = refundService.getAllRefunds(customerId); @@ -90,8 +87,8 @@ public class RefundController { Long customerId = null; if (role != null && role.equals("CUSTOMER")) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - customerId = customer.getCustomerId(); + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + customerId = user.getId(); } RefundResponse refund = refundService.getRefundById(id, customerId); diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 3d127c19..c60c3fcd 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -27,8 +27,6 @@ public class AppointmentRequest { private List petIds; - private List customerPetIds; - private Long employeeId; public Long getCustomerId() { @@ -87,14 +85,6 @@ public class AppointmentRequest { this.petIds = petIds; } - public List getCustomerPetIds() { - return customerPetIds; - } - - public void setCustomerPetIds(List customerPetIds) { - this.customerPetIds = customerPetIds; - } - public Long getEmployeeId() { return employeeId; } @@ -115,13 +105,12 @@ public class AppointmentRequest { Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petIds, that.petIds) && - Objects.equals(customerPetIds, that.customerPetIds) && Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, customerPetIds, employeeId); + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, employeeId); } @Override @@ -134,7 +123,6 @@ public class AppointmentRequest { ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + ", petIds=" + petIds + - ", customerPetIds=" + customerPetIds + ", employeeId=" + employeeId + '}'; } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java index efc1c300..f8e14ac2 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -21,8 +21,6 @@ public class AppointmentResponse { private String employeeName; private List petNames; private List petIds; - private List customerPetNames; - private List customerPetIds; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -158,24 +156,6 @@ public class AppointmentResponse { this.petIds = petIds; } - public List getCustomerPetNames() { - - return customerPetNames; - } - - public void setCustomerPetNames(List customerPetNames) { - this.customerPetNames = customerPetNames; - } - - public List getCustomerPetIds() { - - return customerPetIds; - } - - public void setCustomerPetIds(List customerPetIds) { - this.customerPetIds = customerPetIds; - } - public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java deleted file mode 100644 index ded898e3..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerRequest.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.petshop.backend.dto.customer; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import java.util.Objects; - -public class CustomerRequest { - @NotBlank(message = "First name is required") - private String firstName; - - @NotBlank(message = "Last name is required") - private String lastName; - - @Email(message = "Invalid email format") - private String email; - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CustomerRequest that = (CustomerRequest) o; - return Objects.equals(firstName, that.firstName) && - Objects.equals(lastName, that.lastName) && - Objects.equals(email, that.email); - } - - @Override - public int hashCode() { - return Objects.hash(firstName, lastName, email); - } - - @Override - public String toString() { - return "CustomerRequest{" + - "firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", email='" + email + '\'' + - '}'; - } -} diff --git a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java b/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java deleted file mode 100644 index bd05bf76..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/customer/CustomerResponse.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.petshop.backend.dto.customer; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class CustomerResponse { - private Long customerId; - private String firstName; - private String lastName; - private String email; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public CustomerResponse() { - } - - public CustomerResponse(Long customerId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.customerId = customerId; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public Long getCustomerId() { - return customerId; - } - - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CustomerResponse that = (CustomerResponse) o; - return Objects.equals(customerId, that.customerId) && Objects.equals(firstName, that.firstName) && Objects.equals(lastName, that.lastName) && Objects.equals(email, that.email) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); - } - - @Override - public int hashCode() { - return Objects.hash(customerId, firstName, lastName, email, createdAt, updatedAt); - } - - @Override - public String toString() { - return "CustomerResponse{" + - "customerId=" + customerId + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", email='" + email + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; - } -} diff --git a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java deleted file mode 100644 index b4b37355..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetRequest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.petshop.backend.dto.customerpet; - -import jakarta.validation.constraints.NotBlank; - -import java.util.Objects; - -public class CustomerPetRequest { - - @NotBlank(message = "Pet name is required") - private String petName; - - @NotBlank(message = "Species is required") - private String species; - - private String breed; - - public String getPetName() { - - return petName; - } - - public void setPetName(String petName) { - this.petName = petName; - } - - public String getSpecies() { - - return species; - } - - public void setSpecies(String species) { - this.species = species; - } - - public String getBreed() { - - return breed; - } - - public void setBreed(String breed) { - this.breed = breed; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - - return true; - } - - if (o == null || getClass() != o.getClass()) { - - return false; - } - - CustomerPetRequest that = (CustomerPetRequest) o; - - return Objects.equals(petName, that.petName) && Objects.equals(species, that.species) && Objects.equals(breed, that.breed); - } - - @Override - public int hashCode() { - - return Objects.hash(petName, species, breed); - } -} diff --git a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java deleted file mode 100644 index 8e9ab37d..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/customerpet/CustomerPetResponse.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.petshop.backend.dto.customerpet; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class CustomerPetResponse { - - private Long customerPetId; - private Long customerId; - private String petName; - private String species; - private String breed; - private String imageUrl; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public CustomerPetResponse() { - } - - public CustomerPetResponse(Long customerPetId, Long customerId, String petName, String species, String breed, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.customerPetId = customerPetId; - this.customerId = customerId; - this.petName = petName; - this.species = species; - this.breed = breed; - this.imageUrl = imageUrl; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public Long getCustomerPetId() { - - return customerPetId; - } - - public void setCustomerPetId(Long customerPetId) { - this.customerPetId = customerPetId; - } - - public Long getCustomerId() { - - return customerId; - } - - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } - - public String getPetName() { - - return petName; - } - - public void setPetName(String petName) { - this.petName = petName; - } - - public String getSpecies() { - - return species; - } - - public void setSpecies(String species) { - this.species = species; - } - - public String getBreed() { - - return breed; - } - - public void setBreed(String breed) { - this.breed = breed; - } - - public String getImageUrl() { - - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDateTime getCreatedAt() { - - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - CustomerPetResponse that = (CustomerPetResponse) o; - - return Objects.equals(customerPetId, that.customerPetId); - } - - @Override - public int hashCode() { - - return Objects.hash(customerPetId); - } -} diff --git a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java deleted file mode 100644 index f5fb9020..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeRequest.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.petshop.backend.dto.employee; - -import com.petshop.backend.entity.User; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -public class EmployeeRequest { - @NotBlank(message = "Username is required") - @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") - private String username; - - @Size(min = 6, message = "Password must be at least 6 characters") - private String password; - - @NotBlank(message = "First name is required") - private String firstName; - - @NotBlank(message = "Last name is required") - private String lastName; - - @Email(message = "Invalid email format") - private String email; - - @NotBlank(message = "Phone is required") - @Size(max = 20, message = "Phone must not exceed 20 characters") - private String phone; - - @NotNull(message = "Role is required") - private User.Role role; - - private Boolean active = true; - - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - public String getPassword() { return password; } - public void setPassword(String password) { this.password = password; } - public String getFirstName() { return firstName; } - public void setFirstName(String firstName) { this.firstName = firstName; } - public String getLastName() { return lastName; } - public void setLastName(String lastName) { this.lastName = lastName; } - public String getEmail() { return email; } - public void setEmail(String email) { this.email = email; } - public String getPhone() { return phone; } - public void setPhone(String phone) { this.phone = phone; } - public User.Role getRole() { return role; } - public void setRole(User.Role role) { this.role = role; } - public Boolean getActive() { return active; } - public void setActive(Boolean active) { this.active = active; } -} diff --git a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java b/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java deleted file mode 100644 index a159fc35..00000000 --- a/backend/src/main/java/com/petshop/backend/dto/employee/EmployeeResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.petshop.backend.dto.employee; - -import java.time.LocalDateTime; - -public class EmployeeResponse { - private Long employeeId; - private Long userId; - private String username; - private String firstName; - private String lastName; - private String fullName; - private String email; - private String phone; - private String role; - private Boolean active; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - public Long getEmployeeId() { return employeeId; } - public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } - public Long getUserId() { return userId; } - public void setUserId(Long userId) { this.userId = userId; } - public String getUsername() { return username; } - public void setUsername(String username) { this.username = username; } - public String getFirstName() { return firstName; } - public void setFirstName(String firstName) { this.firstName = firstName; } - public String getLastName() { return lastName; } - public void setLastName(String lastName) { this.lastName = lastName; } - public String getFullName() { return fullName; } - public void setFullName(String fullName) { this.fullName = fullName; } - public String getEmail() { return email; } - public void setEmail(String email) { this.email = email; } - public String getPhone() { return phone; } - public void setPhone(String phone) { this.phone = phone; } - public String getRole() { return role; } - public void setRole(String role) { this.role = role; } - public Boolean getActive() { return active; } - public void setActive(Boolean active) { this.active = active; } - public LocalDateTime getCreatedAt() { return createdAt; } - public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } - public LocalDateTime getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } -} diff --git a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java index 211f75de..7c778bc3 100644 --- a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java +++ b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java @@ -14,8 +14,8 @@ public class ActivityLog { private Long logId; @ManyToOne - @JoinColumn(name = "employeeId", nullable = false) - private Employee employee; + @JoinColumn(name = "userId", nullable = false) + private User user; @Column(nullable = false, columnDefinition = "TEXT") private String activity; @@ -26,9 +26,9 @@ public class ActivityLog { public ActivityLog() { } - public ActivityLog(Long logId, Employee employee, String activity, LocalDateTime logTimestamp) { + public ActivityLog(Long logId, User user, String activity, LocalDateTime logTimestamp) { this.logId = logId; - this.employee = employee; + this.user = user; this.activity = activity; this.logTimestamp = logTimestamp; } @@ -41,12 +41,12 @@ public class ActivityLog { this.logId = logId; } - public Employee getEmployee() { - return employee; + public User getUser() { + return user; } - public void setEmployee(Employee employee) { - this.employee = employee; + public void setUser(User user) { + this.user = user; } public String getActivity() { @@ -82,7 +82,7 @@ public class ActivityLog { public String toString() { return "ActivityLog{" + "logId=" + logId + - ", employee=" + employee + + ", user=" + user + ", activity='" + activity + '\'' + ", logTimestamp=" + logTimestamp + '}'; diff --git a/backend/src/main/java/com/petshop/backend/entity/Adoption.java b/backend/src/main/java/com/petshop/backend/entity/Adoption.java index 78360e2e..35d4edac 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Adoption.java +++ b/backend/src/main/java/com/petshop/backend/entity/Adoption.java @@ -4,7 +4,6 @@ import jakarta.persistence.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Objects; @@ -23,11 +22,11 @@ public class Adoption { @ManyToOne @JoinColumn(name = "customerId", nullable = false) - private Customer customer; + private User customer; @ManyToOne @JoinColumn(name = "employeeId", nullable = false) - private Employee employee; + private User employee; @Column(nullable = false) private LocalDate adoptionDate; @@ -46,17 +45,6 @@ public class Adoption { public Adoption() { } - public Adoption(Long adoptionId, Pet pet, Customer customer, Employee employee, LocalDate adoptionDate, String adoptionStatus, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.adoptionId = adoptionId; - this.pet = pet; - this.customer = customer; - this.employee = employee; - this.adoptionDate = adoptionDate; - this.adoptionStatus = adoptionStatus; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getAdoptionId() { return adoptionId; } @@ -73,19 +61,19 @@ public class Adoption { this.pet = pet; } - public Customer getCustomer() { + public User getCustomer() { return customer; } - public void setCustomer(Customer customer) { + public void setCustomer(User customer) { this.customer = customer; } - public Employee getEmployee() { + public User getEmployee() { return employee; } - public void setEmployee(Employee employee) { + public void setEmployee(User employee) { this.employee = employee; } @@ -138,13 +126,8 @@ public class Adoption { public String toString() { return "Adoption{" + "adoptionId=" + adoptionId + - ", pet=" + pet + - ", customer=" + customer + - ", employee=" + employee + - ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + + ", adoptionDate=" + adoptionDate + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java index d4ebc199..f313d928 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Appointment.java +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -21,7 +21,7 @@ public class Appointment { @ManyToOne @JoinColumn(name = "customerId", nullable = false) - private Customer customer; + private User customer; @ManyToOne @JoinColumn(name = "storeId", nullable = false) @@ -33,7 +33,7 @@ public class Appointment { @ManyToOne @JoinColumn(name = "employeeId", nullable = false) - private Employee employee; + private User employee; @Column(nullable = false) private LocalDate appointmentDate; @@ -52,14 +52,6 @@ public class Appointment { ) private Set pets = new HashSet<>(); - @ManyToMany - @JoinTable( - name = "appointment_customer_pet", - joinColumns = @JoinColumn(name = "appointment_id"), - inverseJoinColumns = @JoinColumn(name = "customer_pet_id") - ) - private Set customerPets = new HashSet<>(); - @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -71,20 +63,6 @@ public class Appointment { public Appointment() { } - public Appointment(Long appointmentId, Customer customer, StoreLocation store, Service service, Employee employee, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, Set pets, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.appointmentId = appointmentId; - this.customer = customer; - this.store = store; - this.service = service; - this.employee = employee; - this.appointmentDate = appointmentDate; - this.appointmentTime = appointmentTime; - this.appointmentStatus = appointmentStatus; - this.pets = pets; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getAppointmentId() { return appointmentId; } @@ -93,11 +71,11 @@ public class Appointment { this.appointmentId = appointmentId; } - public Customer getCustomer() { + public User getCustomer() { return customer; } - public void setCustomer(Customer customer) { + public void setCustomer(User customer) { this.customer = customer; } @@ -117,11 +95,11 @@ public class Appointment { this.service = service; } - public Employee getEmployee() { + public User getEmployee() { return employee; } - public void setEmployee(Employee employee) { + public void setEmployee(User employee) { this.employee = employee; } @@ -157,15 +135,6 @@ public class Appointment { this.pets = pets; } - public Set getCustomerPets() { - - return customerPets; - } - - public void setCustomerPets(Set customerPets) { - this.customerPets = customerPets; - } - public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Customer.java b/backend/src/main/java/com/petshop/backend/entity/Customer.java deleted file mode 100644 index 09035619..00000000 --- a/backend/src/main/java/com/petshop/backend/entity/Customer.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.petshop.backend.entity; - -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; -import java.util.Objects; - -@Entity -@Table(name = "customer") -public class Customer { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long customerId; - - @Column(name = "user_id") - private Long userId; - - @Column(nullable = false, length = 50) - private String firstName; - - @Column(nullable = false, length = 50) - private String lastName; - - @Column(nullable = false, length = 100) - private String email; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - public Customer() { - } - - public Customer(Long customerId, Long userId, String firstName, String lastName, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.customerId = customerId; - this.userId = userId; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public Long getCustomerId() { - return customerId; - } - - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Customer customer = (Customer) o; - return Objects.equals(customerId, customer.customerId); - } - - @Override - public int hashCode() { - return Objects.hash(customerId); - } - - @Override - public String toString() { - return "Customer{" + - "customerId=" + customerId + - ", userId=" + userId + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", email='" + email + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; - } -} diff --git a/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java b/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java deleted file mode 100644 index df75df8c..00000000 --- a/backend/src/main/java/com/petshop/backend/entity/CustomerPet.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.petshop.backend.entity; - -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; -import java.util.Objects; - -@Entity -@Table(name = "customer_pet") -public class CustomerPet { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "customer_pet_id") - private Long customerPetId; - - @ManyToOne - @JoinColumn(name = "customer_id", nullable = false) - private Customer customer; - - @Column(name = "pet_name", nullable = false, length = 50) - private String petName; - - @Column(nullable = false, length = 50) - private String species; - - @Column(length = 50) - private String breed; - - @Column(name = "image_url", length = 255) - private String imageUrl; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - public CustomerPet() { - } - - public Long getCustomerPetId() { - - return customerPetId; - } - - public void setCustomerPetId(Long customerPetId) { - this.customerPetId = customerPetId; - } - - public Customer getCustomer() { - - return customer; - } - - public void setCustomer(Customer customer) { - this.customer = customer; - } - - public String getPetName() { - - return petName; - } - - public void setPetName(String petName) { - this.petName = petName; - } - - public String getSpecies() { - - return species; - } - - public void setSpecies(String species) { - this.species = species; - } - - public String getBreed() { - - return breed; - } - - public void setBreed(String breed) { - this.breed = breed; - } - - public String getImageUrl() { - - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDateTime getCreatedAt() { - - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - CustomerPet that = (CustomerPet) o; - return Objects.equals(customerPetId, that.customerPetId); - } - - @Override - public int hashCode() { - - return Objects.hash(customerPetId); - } -} diff --git a/backend/src/main/java/com/petshop/backend/entity/Employee.java b/backend/src/main/java/com/petshop/backend/entity/Employee.java deleted file mode 100644 index c88216f6..00000000 --- a/backend/src/main/java/com/petshop/backend/entity/Employee.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.petshop.backend.entity; - -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; -import java.util.Objects; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long employeeId; - - @Column(name = "user_id") - private Long userId; - - @Column(nullable = false, length = 50) - private String firstName; - - @Column(nullable = false, length = 50) - private String lastName; - - @Column(nullable = false, length = 100) - private String email; - - @Column(nullable = false, length = 50) - private String role; - - @Column(nullable = false) - private Boolean isActive = true; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - public Employee() { - } - - public Employee(Long employeeId, Long userId, String firstName, String lastName, String email, String role, Boolean isActive, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.employeeId = employeeId; - this.userId = userId; - this.firstName = firstName; - this.lastName = lastName; - this.email = email; - this.role = role; - this.isActive = isActive; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public Long getEmployeeId() { - return employeeId; - } - - public void setEmployeeId(Long employeeId) { - this.employeeId = employeeId; - } - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } - - public Boolean getIsActive() { - return isActive; - } - - public void setIsActive(Boolean isActive) { - this.isActive = isActive; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Employee employee = (Employee) o; - return Objects.equals(employeeId, employee.employeeId); - } - - @Override - public int hashCode() { - return Objects.hash(employeeId); - } - - @Override - public String toString() { - return "Employee{" + - "employeeId=" + employeeId + - ", userId=" + userId + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", email='" + email + '\'' + - ", role='" + role + '\'' + - ", isActive=" + isActive + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; - } -} diff --git a/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java b/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java deleted file mode 100644 index daa2a2e2..00000000 --- a/backend/src/main/java/com/petshop/backend/entity/EmployeeStore.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.petshop.backend.entity; - -import jakarta.persistence.*; - -import java.io.Serializable; -import java.util.Objects; - -@Entity -@Table(name = "employeeStore") -@IdClass(EmployeeStore.EmployeeStoreId.class) -public class EmployeeStore { - - @Id - @ManyToOne - @JoinColumn(name = "employeeId", nullable = false) - private Employee employee; - - @Id - @ManyToOne - @JoinColumn(name = "storeId", nullable = false) - private StoreLocation store; - - public EmployeeStore() { - } - - public EmployeeStore(Employee employee, StoreLocation store) { - this.employee = employee; - this.store = store; - } - - public Employee getEmployee() { - return employee; - } - - public void setEmployee(Employee employee) { - this.employee = employee; - } - - public StoreLocation getStore() { - return store; - } - - public void setStore(StoreLocation store) { - this.store = store; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - EmployeeStore that = (EmployeeStore) o; - return Objects.equals(employee, that.employee) && Objects.equals(store, that.store); - } - - @Override - public int hashCode() { - return Objects.hash(employee, store); - } - - @Override - public String toString() { - return "EmployeeStore{" + - "employee=" + employee + - ", store=" + store + - '}'; - } - - public static class EmployeeStoreId implements Serializable { - private Long employee; - private Long store; - - public EmployeeStoreId() { - } - - public EmployeeStoreId(Long employee, Long store) { - this.employee = employee; - this.store = store; - } - - public Long getEmployee() { - return employee; - } - - public void setEmployee(Long employee) { - this.employee = employee; - } - - public Long getStore() { - return store; - } - - public void setStore(Long store) { - this.store = store; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - EmployeeStoreId that = (EmployeeStoreId) o; - return Objects.equals(employee, that.employee) && Objects.equals(store, that.store); - } - - @Override - public int hashCode() { - return Objects.hash(employee, store); - } - - @Override - public String toString() { - return "EmployeeStoreId{" + - "employee=" + employee + - ", store=" + store + - '}'; - } - } -} diff --git a/backend/src/main/java/com/petshop/backend/entity/Pet.java b/backend/src/main/java/com/petshop/backend/entity/Pet.java index d0b3b3fc..604a24db 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Pet.java +++ b/backend/src/main/java/com/petshop/backend/entity/Pet.java @@ -23,24 +23,24 @@ public class Pet { @Column(nullable = false, length = 50) private String petSpecies; - @Column(nullable = false, length = 50) + @Column(length = 50) private String petBreed; - @Column(nullable = false) + @Column private Integer petAge; @Column(nullable = false, length = 20) private String petStatus; - @Column(nullable = false, precision = 10, scale = 2) + @Column(precision = 10, scale = 2) private BigDecimal petPrice; @Column(length = 255) private String imageUrl; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "customerId") - private Customer customer; + @JoinColumn(name = "ownerUserId") + private User owner; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "storeId") @@ -57,19 +57,6 @@ public class Pet { public Pet() { } - public Pet(Long id, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.petName = petName; - this.petSpecies = petSpecies; - this.petBreed = petBreed; - this.petAge = petAge; - this.petStatus = petStatus; - this.petPrice = petPrice; - this.imageUrl = imageUrl; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getPetId() { return id; } @@ -134,6 +121,22 @@ public class Pet { this.imageUrl = imageUrl; } + public User getOwner() { + return owner; + } + + public void setOwner(User owner) { + this.owner = owner; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + public LocalDateTime getCreatedAt() { return createdAt; } @@ -150,22 +153,6 @@ public class Pet { this.updatedAt = updatedAt; } - public Customer getCustomer() { - return customer; - } - - public void setCustomer(Customer customer) { - this.customer = customer; - } - - public StoreLocation getStore() { - return store; - } - - public void setStore(StoreLocation store) { - this.store = store; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -185,13 +172,7 @@ public class Pet { "id=" + id + ", petName='" + petName + '\'' + ", petSpecies='" + petSpecies + '\'' + - ", petBreed='" + petBreed + '\'' + - ", petAge=" + petAge + ", petStatus='" + petStatus + '\'' + - ", petPrice=" + petPrice + - ", imageUrl='" + imageUrl + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java index c60c0927..ee1a9d51 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Sale.java +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -23,7 +23,7 @@ public class Sale { @ManyToOne @JoinColumn(name = "employeeId", nullable = false) - private Employee employee; + private User employee; @ManyToOne @JoinColumn(name = "storeId", nullable = false) @@ -31,7 +31,7 @@ public class Sale { @ManyToOne @JoinColumn(name = "customerId") - private Customer customer; + private User customer; @Column(nullable = false, precision = 10, scale = 2) private BigDecimal totalAmount; @@ -60,21 +60,6 @@ public class Sale { public Sale() { } - public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, Customer customer, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List items, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.saleId = saleId; - this.saleDate = saleDate; - this.employee = employee; - this.store = store; - this.customer = customer; - this.totalAmount = totalAmount; - this.paymentMethod = paymentMethod; - this.isRefund = isRefund; - this.originalSale = originalSale; - this.items = items; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public Long getSaleId() { return saleId; } @@ -91,11 +76,11 @@ public class Sale { this.saleDate = saleDate; } - public Employee getEmployee() { + public User getEmployee() { return employee; } - public void setEmployee(Employee employee) { + public void setEmployee(User employee) { this.employee = employee; } @@ -107,11 +92,11 @@ public class Sale { this.store = store; } - public Customer getCustomer() { + public User getCustomer() { return customer; } - public void setCustomer(Customer customer) { + public void setCustomer(User customer) { this.customer = customer; } @@ -189,16 +174,9 @@ public class Sale { return "Sale{" + "saleId=" + saleId + ", saleDate=" + saleDate + - ", employee=" + employee + - ", store=" + store + - ", customer=" + customer + ", totalAmount=" + totalAmount + ", paymentMethod='" + paymentMethod + '\'' + ", isRefund=" + isRefund + - ", originalSale=" + originalSale + - ", items=" + items + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 7b632f7f..7502ec33 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -19,9 +19,9 @@ public interface AdoptionRepository extends JpaRepository { "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchAdoptions(@Param("q") String query, Pageable pageable); - Page findByCustomerCustomerId(Long customerId, Pageable pageable); + Page findByCustomerId(Long customerId, Pageable pageable); - @Query("SELECT a FROM Adoption a WHERE a.customer.customerId = :customerId AND (" + + @Query("SELECT a FROM Adoption a WHERE a.customer.id = :customerId AND (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))") diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index f8649671..f4cdc564 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -28,18 +28,18 @@ public interface AppointmentRepository extends JpaRepository "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchAppointments(@Param("q") String query, Pageable pageable); - Page findByCustomerCustomerId(Long customerId, Pageable pageable); + Page findByCustomerId(Long customerId, Pageable pageable); - @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE a.customer.customerId = :customerId AND (" + + @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE a.customer.id = :customerId AND (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") - List findByEmployeeEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); - @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.employeeId IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") - List findByEmployeeEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); + @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id IN :employeeIds AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") + List findByEmployeeIdInAndAppointmentDate(@Param("employeeIds") List employeeIds, @Param("date") LocalDate date); } diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java deleted file mode 100644 index 4fe0ef81..00000000 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerPetRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.petshop.backend.repository; - -import com.petshop.backend.entity.CustomerPet; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface CustomerPetRepository extends JpaRepository { - - List findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId); - - List findByCustomerCustomerIdOrderByPetNameAsc(Long customerId); - - Optional findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId); -} diff --git a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java b/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java deleted file mode 100644 index 2c860de7..00000000 --- a/backend/src/main/java/com/petshop/backend/repository/CustomerRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.petshop.backend.repository; - -import com.petshop.backend.entity.Customer; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface CustomerRepository extends JpaRepository { - - Optional findByUserId(Long userId); - List findAllByEmail(String email); - - @Query("SELECT DISTINCT c FROM Customer c WHERE EXISTS (SELECT cp FROM CustomerPet cp WHERE cp.customer = c) ORDER BY c.firstName ASC, c.lastName ASC") - List findAllWithPets(); - - @Query("SELECT c FROM Customer c WHERE " + - "LOWER(c.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(c.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(c.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "EXISTS (SELECT u FROM User u WHERE u.id = c.userId AND LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%')))") - Page searchCustomers(@Param("q") String query, Pageable pageable); -} diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java deleted file mode 100644 index e320fc00..00000000 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.petshop.backend.repository; - -import com.petshop.backend.entity.Employee; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByUserId(Long userId); - List findAllByEmail(String email); - Optional findFirstByIsActiveTrueOrderByEmployeeIdAsc(); - List findAllByIsActiveTrueOrderByEmployeeIdAsc(); - - @Query("SELECT e FROM Employee e WHERE " + - "LOWER(e.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(e.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(e.email) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(e.role) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "EXISTS (SELECT u FROM User u WHERE u.id = e.userId AND (" + - "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))))") - Page searchEmployees(@Param("q") String query, Pageable pageable); -} diff --git a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java b/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java deleted file mode 100644 index 16a59cea..00000000 --- a/backend/src/main/java/com/petshop/backend/repository/EmployeeStoreRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.petshop.backend.repository; - -import com.petshop.backend.entity.EmployeeStore; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EmployeeStoreRepository extends JpaRepository { - Optional findByEmployeeEmployeeId(Long employeeId); - - @Query("SELECT es FROM EmployeeStore es WHERE es.store.storeId = :storeId AND es.employee.isActive = true ORDER BY es.employee.employeeId ASC") - List findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(@Param("storeId") Long storeId); - - @Query("SELECT es FROM EmployeeStore es WHERE es.employee.isActive = true ORDER BY es.employee.employeeId ASC") - List findActiveAllOrderByEmployeeEmployeeIdAsc(); -} diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index d01c2d85..fd55fabe 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -16,21 +16,21 @@ public interface PetRepository extends JpaRepository { List findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus); @Query("SELECT p FROM Pet p WHERE " + - "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " + "(:storeId IS NULL OR p.store.storeId = :storeId)") Page searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, Pageable pageable); @Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " + - "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:storeId IS NULL OR p.store.storeId = :storeId)") Page searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("storeId") Long storeId, Pageable pageable); @Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " + - "(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId OR (LOWER(p.petStatus) = 'owned' AND p.customer.userId = :userId)) AND " + - "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(LOWER(p.petStatus) = 'available' OR a.customer.id = :userId OR (LOWER(p.petStatus) = 'owned' AND p.owner.id = :userId)) AND " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))") Page searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable); diff --git a/backend/src/main/java/com/petshop/backend/repository/UserRepository.java b/backend/src/main/java/com/petshop/backend/repository/UserRepository.java index 6bec352f..592a4a76 100644 --- a/backend/src/main/java/com/petshop/backend/repository/UserRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/UserRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -17,16 +18,24 @@ public interface UserRepository extends JpaRepository { Optional findByPhone(String phone); boolean existsByUsername(String username); Page findByRole(User.Role role, Pageable pageable); + List findByRoleAndActiveTrue(User.Role role); + List findByPrimaryStoreStoreIdAndRoleAndActiveTrue(Long storeId, User.Role role); + Optional findFirstByPrimaryStoreStoreIdAndRoleAndActiveTrueOrderByIdAsc(Long storeId, User.Role role); + Optional findFirstByRoleAndActiveTrueOrderByIdAsc(User.Role role); @Query("SELECT u FROM User u WHERE " + - "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.username, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.firstName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.fullName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.email, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchUsers(@Param("q") String query, Pageable pageable); @Query("SELECT u FROM User u WHERE u.role = :role AND (" + - "LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.username, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.firstName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(COALESCE(u.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.fullName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.email, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(COALESCE(u.phone, '')) LIKE LOWER(CONCAT('%', :q, '%')))") diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index c6ff1c60..caadfbb9 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -4,14 +4,10 @@ import com.petshop.backend.dto.adoption.AdoptionRequest; import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AdoptionRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; @@ -30,15 +26,11 @@ public class AdoptionService { private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; - private final CustomerRepository customerRepository; - private final EmployeeRepository employeeRepository; private final UserRepository userRepository; - public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, CustomerRepository customerRepository, EmployeeRepository employeeRepository, UserRepository userRepository) { + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository) { this.adoptionRepository = adoptionRepository; this.petRepository = petRepository; - this.customerRepository = customerRepository; - this.employeeRepository = employeeRepository; this.userRepository = userRepository; } @@ -49,7 +41,7 @@ public class AdoptionService { if (query != null && !query.trim().isEmpty()) { adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable); } else { - adoptions = adoptionRepository.findByCustomerCustomerId(customerId, pageable); + adoptions = adoptionRepository.findByCustomerId(customerId, pageable); } } else { if (query != null && !query.trim().isEmpty()) { @@ -66,7 +58,7 @@ public class AdoptionService { Adoption adoption = adoptionRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id)); - if (customerId != null && !adoption.getCustomer().getCustomerId().equals(customerId)) { + if (customerId != null && !adoption.getCustomer().getId().equals(customerId)) { throw new ResourceNotFoundException("You can only view your own adoptions"); } @@ -78,9 +70,9 @@ public class AdoptionService { Pet pet = petRepository.findById(request.getPetId()) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + request.getPetId())); - Customer customer = customerRepository.findById(request.getCustomerId()) + User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); - Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + User employee = resolveAdoptionEmployee(request.getEmployeeId()); String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); validatePetAvailability(pet, null); @@ -104,9 +96,9 @@ public class AdoptionService { Pet pet = petRepository.findById(request.getPetId()) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + request.getPetId())); - Customer customer = customerRepository.findById(request.getCustomerId()) + User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); - Employee employee = resolveAdoptionEmployee(request.getEmployeeId()); + User employee = resolveAdoptionEmployee(request.getEmployeeId()); String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); validatePetAvailability(pet, adoption.getAdoptionId()); @@ -139,9 +131,9 @@ public class AdoptionService { adoption.getAdoptionId(), adoption.getPet().getPetId(), adoption.getPet().getPetName(), - adoption.getCustomer().getCustomerId(), + adoption.getCustomer().getId(), adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), - adoption.getEmployee().getEmployeeId(), + adoption.getEmployee().getId(), adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(), adoption.getAdoptionDate(), adoption.getAdoptionStatus(), @@ -151,31 +143,22 @@ public class AdoptionService { ); } - private Employee resolveAdoptionEmployee(Long requestedEmployeeId) { + private User resolveAdoptionEmployee(Long requestedEmployeeId) { if (requestedEmployeeId != null) { - Employee employee = employeeRepository.findById(requestedEmployeeId) + User employee = userRepository.findById(requestedEmployeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); - if (!isAssignableEmployee(employee)) { + if (!isAssignableUser(employee)) { throw new IllegalArgumentException("Selected employee is not assignable for adoption work"); } return employee; } - return employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc().stream() - .filter(this::isAssignableEmployee) - .findFirst() + return userRepository.findFirstByRoleAndActiveTrueOrderByIdAsc(User.Role.STAFF) .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is available for adoption assignment")); } - private boolean isAssignableEmployee(Employee employee) { - Long userId = employee.getUserId(); - if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { - return false; - } - return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) - .filter(user -> Boolean.TRUE.equals(user.getActive())) - .isPresent(); + private boolean isAssignableUser(User user) { + return user.getRole() == User.Role.STAFF && Boolean.TRUE.equals(user.getActive()); } private String normalizeAdoptionStatus(String adoptionStatus) { diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java index c14a9511..f4841228 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -1,12 +1,10 @@ package com.petshop.backend.service; import com.petshop.backend.dto.analytics.DashboardResponse; -import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.Inventory; import com.petshop.backend.entity.Product; import com.petshop.backend.entity.Sale; import com.petshop.backend.entity.User; -import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.InventoryRepository; import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.SaleRepository; @@ -26,14 +24,12 @@ public class AnalyticsService { private final SaleRepository saleRepository; private final InventoryRepository inventoryRepository; private final ProductRepository productRepository; - private final EmployeeRepository employeeRepository; public AnalyticsService(SaleRepository saleRepository, - InventoryRepository inventoryRepository, ProductRepository productRepository, EmployeeRepository employeeRepository) { + InventoryRepository inventoryRepository, ProductRepository productRepository) { this.saleRepository = saleRepository; this.inventoryRepository = inventoryRepository; this.productRepository = productRepository; - this.employeeRepository = employeeRepository; } @Transactional(readOnly = true) @@ -183,11 +179,8 @@ public class AnalyticsService { } if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) { - Employee employee = employeeRepository.findByUserId(user.getId()).orElse(null); - if (employee != null) { - String employeeName = employee.getFirstName() + " " + employee.getLastName(); - employeeRevenue.put(employeeName, BigDecimal.ZERO); - } + String employeeName = user.getFirstName() + " " + user.getLastName(); + employeeRevenue.put(employeeName, BigDecimal.ZERO); } return employeeRevenue.entrySet().stream() @@ -200,7 +193,7 @@ public class AnalyticsService { return true; } if (user.getRole() == User.Role.STAFF) { - return sale.getEmployee() != null && sale.getEmployee().getUserId() != null && sale.getEmployee().getUserId().equals(user.getId()); + return sale.getEmployee() != null && sale.getEmployee().getId() != null && sale.getEmployee().getId().equals(user.getId()); } return false; } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 155b7524..d3a8594e 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -4,19 +4,11 @@ import com.petshop.backend.dto.appointment.AppointmentRequest; import com.petshop.backend.dto.appointment.AppointmentResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Appointment; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.CustomerPet; -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.EmployeeStore; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AppointmentRepository; -import com.petshop.backend.repository.CustomerPetRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; -import com.petshop.backend.repository.EmployeeStoreRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; @@ -41,25 +33,17 @@ import java.util.stream.Collectors; public class AppointmentService { private final AppointmentRepository appointmentRepository; - private final CustomerRepository customerRepository; - private final CustomerPetRepository customerPetRepository; private final ServiceRepository serviceRepository; private final PetRepository petRepository; private final StoreRepository storeRepository; private final UserRepository userRepository; - private final EmployeeRepository employeeRepository; - private final EmployeeStoreRepository employeeStoreRepository; - public AppointmentService(AppointmentRepository appointmentRepository, CustomerRepository customerRepository, CustomerPetRepository customerPetRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) { + public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository) { this.appointmentRepository = appointmentRepository; - this.customerRepository = customerRepository; - this.customerPetRepository = customerPetRepository; this.serviceRepository = serviceRepository; this.petRepository = petRepository; this.storeRepository = storeRepository; this.userRepository = userRepository; - this.employeeRepository = employeeRepository; - this.employeeStoreRepository = employeeStoreRepository; } @Transactional(readOnly = true) @@ -70,7 +54,7 @@ public class AppointmentService { if (query != null && !query.trim().isEmpty()) { appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable); } else { - appointments = appointmentRepository.findByCustomerCustomerId(customerId, pageable); + appointments = appointmentRepository.findByCustomerId(customerId, pageable); } } else { if (query != null && !query.trim().isEmpty()) { @@ -88,7 +72,7 @@ public class AppointmentService { Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); - if (customerId != null && !appointment.getCustomer().getCustomerId().equals(customerId)) { + if (customerId != null && !appointment.getCustomer().getId().equals(customerId)) { throw new ResourceNotFoundException("You can only view your own appointments"); } @@ -101,7 +85,7 @@ public class AppointmentService { User authenticatedUser = AuthenticationHelper.getAuthenticatedUser(userRepository); - Customer customer = customerRepository.findById(request.getCustomerId()) + User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); StoreLocation store = storeRepository.findById(request.getStoreId()) @@ -111,15 +95,13 @@ public class AppointmentService { .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); - boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); - if (!hasPetIds && !hasCustomerPetIds) { + if (!hasPetIds) { throw new IllegalArgumentException("Please specify at least one pet."); } - Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); - Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + Set pets = fetchPets(request.getPetIds()); + User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); @@ -132,7 +114,6 @@ public class AppointmentService { appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); - appointment.setCustomerPets(customerPets); appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); @@ -148,7 +129,7 @@ public class AppointmentService { Appointment appointment = appointmentRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + id)); - Customer customer = customerRepository.findById(request.getCustomerId()) + User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); StoreLocation store = storeRepository.findById(request.getStoreId()) @@ -158,15 +139,13 @@ public class AppointmentService { .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); - boolean hasCustomerPetIds = request.getCustomerPetIds() != null && !request.getCustomerPetIds().isEmpty(); - if (!hasPetIds && !hasCustomerPetIds) { + if (!hasPetIds) { throw new IllegalArgumentException("Please specify at least one pet."); } - Set pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>(); - Set customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>(); - Employee employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); + Set pets = fetchPets(request.getPetIds()); + User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); validateStoreAccess(store.getStoreId(), authenticatedUser); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id); @@ -178,7 +157,6 @@ public class AppointmentService { appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); appointment.setPets(pets); - appointment.setCustomerPets(customerPets); appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); @@ -206,20 +184,17 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(serviceId) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + serviceId)); - List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() - .filter(es -> isAssignableEmployee(es.getEmployee())) - .map(EmployeeStore::getEmployee) - .collect(Collectors.toList()); + List assignableUsers = userRepository.findByPrimaryStoreStoreIdAndRoleAndActiveTrue(storeId, User.Role.STAFF); - if (assignableEmployees.isEmpty()) { + if (assignableUsers.isEmpty()) { return List.of(); } - List employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList()); - List allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date); - + List employeeIds = assignableUsers.stream().map(User::getId).collect(Collectors.toList()); + List allAppointments = appointmentRepository.findByEmployeeIdInAndAppointmentDate(employeeIds, date); + java.util.Map> appointmentsByEmployee = allAppointments.stream() - .collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId())); + .collect(Collectors.groupingBy(a -> a.getEmployee().getId())); List availableSlots = new ArrayList<>(); LocalTime startTime = LocalTime.of(9, 0); @@ -229,8 +204,8 @@ public class AppointmentService { LocalTime currentTime = startTime; while (!currentTime.isAfter(latestStart)) { final LocalTime slotTime = currentTime; - boolean anyEmployeeAvailable = assignableEmployees.stream().anyMatch(emp -> { - List empAppointments = appointmentsByEmployee.getOrDefault(emp.getEmployeeId(), List.of()); + boolean anyEmployeeAvailable = assignableUsers.stream().anyMatch(emp -> { + List empAppointments = appointmentsByEmployee.getOrDefault(emp.getId(), List.of()); return isSlotAvailable(empAppointments, service, slotTime, null); }); @@ -262,20 +237,6 @@ public class AppointmentService { return pets; } - private Set fetchCustomerPets(List customerPetIds, Long customerId) { - Set customerPets = new HashSet<>(); - for (Long customerPetId : customerPetIds) { - CustomerPet customerPet = customerPetRepository.findById(customerPetId) - .orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId)); - if (!customerPet.getCustomer().getCustomerId().equals(customerId)) { - throw new IllegalArgumentException("Selected pet does not belong to the selected customer"); - } - customerPets.add(customerPet); - } - - return customerPets; - } - private AppointmentResponse mapToResponse(Appointment appointment) { List petNames = appointment.getPets().stream() .map(Pet::getPetName) @@ -285,17 +246,9 @@ public class AppointmentService { .map(Pet::getPetId) .collect(Collectors.toList()); - List customerPetNames = appointment.getCustomerPets().stream() - .map(CustomerPet::getPetName) - .collect(Collectors.toList()); - - List customerPetIds = appointment.getCustomerPets().stream() - .map(CustomerPet::getCustomerPetId) - .collect(Collectors.toList()); - AppointmentResponse response = new AppointmentResponse(); response.setAppointmentId(appointment.getAppointmentId()); - response.setCustomerId(appointment.getCustomer().getCustomerId()); + response.setCustomerId(appointment.getCustomer().getId()); response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName()); response.setStoreId(appointment.getStore().getStoreId()); response.setStoreName(appointment.getStore().getStoreName()); @@ -304,54 +257,38 @@ public class AppointmentService { response.setAppointmentDate(appointment.getAppointmentDate()); response.setAppointmentTime(appointment.getAppointmentTime()); response.setAppointmentStatus(appointment.getAppointmentStatus()); - response.setEmployeeId(appointment.getEmployee().getEmployeeId()); + response.setEmployeeId(appointment.getEmployee().getId()); response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); response.setPetNames(petNames); response.setPetIds(petIds); - response.setCustomerPetNames(customerPetNames); - response.setCustomerPetIds(customerPetIds); response.setCreatedAt(appointment.getCreatedAt()); response.setUpdatedAt(appointment.getUpdatedAt()); - + return response; } - private Employee resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) { - List assignableEmployees = employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(storeId).stream() - .filter(es -> isAssignableEmployee(es.getEmployee())) - .collect(Collectors.toList()); + private User resolveAppointmentEmployee(Long requestedEmployeeId, Long storeId) { + List assignableUsers = userRepository.findByPrimaryStoreStoreIdAndRoleAndActiveTrue(storeId, User.Role.STAFF); if (requestedEmployeeId != null) { - Employee employee = employeeRepository.findById(requestedEmployeeId) + User employee = userRepository.findById(requestedEmployeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); - boolean assignedToStore = assignableEmployees.stream() - .anyMatch(es -> es.getEmployee().getEmployeeId().equals(requestedEmployeeId)); + boolean assignedToStore = assignableUsers.stream() + .anyMatch(u -> u.getId().equals(requestedEmployeeId)); if (!assignedToStore) { throw new IllegalArgumentException("Selected employee is not assignable for the selected store"); } return employee; } - return assignableEmployees.stream() - .map(EmployeeStore::getEmployee) + return assignableUsers.stream() .findFirst() .orElseThrow(() -> new IllegalArgumentException("No assignable staff member is assigned to the selected store")); } - private boolean isAssignableEmployee(Employee employee) { - Long userId = employee.getUserId(); - if (userId == null || !Boolean.TRUE.equals(employee.getIsActive())) { - return false; - } - return userRepository.findById(userId) - .filter(user -> user.getRole() == User.Role.STAFF) - .filter(user -> Boolean.TRUE.equals(user.getActive())) - .isPresent(); - } - - private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { + private void validateAvailability(User employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) { List existingAppointments = appointmentRepository - .findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date); + .findByEmployeeIdAndAppointmentDate(employee.getId(), date); if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { throw new IllegalArgumentException("The selected employee is already booked for this time slot"); } @@ -377,11 +314,7 @@ public class AppointmentService { return; } - Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository); - EmployeeStore employeeStore = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()) - .orElseThrow(() -> new AccessDeniedException("Authenticated staff member is not assigned to a store")); - - if (!employeeStore.getStore().getStoreId().equals(requestedStoreId)) { + if (user.getPrimaryStore() == null || !user.getPrimaryStore().getStoreId().equals(requestedStoreId)) { throw new AccessDeniedException("Staff can only manage appointments for their assigned store"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java b/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java index 9b835823..b5b808c2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatRealtimeService.java @@ -3,11 +3,9 @@ package com.petshop.backend.service; import com.petshop.backend.dto.chat.ConversationResponse; import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.entity.Conversation; -import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.ConversationRepository; -import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.MessageRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -21,14 +19,12 @@ public class ChatRealtimeService { private final SimpMessagingTemplate messagingTemplate; private final ConversationRepository conversationRepository; private final MessageRepository messageRepository; - private final CustomerRepository customerRepository; private final UserRepository userRepository; - public ChatRealtimeService(SimpMessagingTemplate messagingTemplate, ConversationRepository conversationRepository, MessageRepository messageRepository, CustomerRepository customerRepository, UserRepository userRepository) { + public ChatRealtimeService(SimpMessagingTemplate messagingTemplate, ConversationRepository conversationRepository, MessageRepository messageRepository, UserRepository userRepository) { this.messagingTemplate = messagingTemplate; this.conversationRepository = conversationRepository; this.messageRepository = messageRepository; - this.customerRepository = customerRepository; this.userRepository = userRepository; } @@ -54,13 +50,11 @@ public class ChatRealtimeService { } private void sendConversationToCustomerQueue(ConversationResponse conversation) { - Customer customer = customerRepository.findById(conversation.getCustomerId()) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found")); - if (customer.getUserId() == null) { + User customerUser = userRepository.findById(conversation.getCustomerId()) + .orElseThrow(() -> new ResourceNotFoundException("User not found")); + if (customerUser.getUsername() == null) { return; } - User customerUser = userRepository.findById(customer.getUserId()) - .orElseThrow(() -> new ResourceNotFoundException("User not found")); messagingTemplate.convertAndSendToUser(customerUser.getUsername(), "/queue/chat/conversations", conversation); } diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index 6ae0c4da..e39d9c55 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -6,12 +6,10 @@ import com.petshop.backend.dto.chat.MessageRequest; import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.Conversation; -import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Message; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.ConversationRepository; -import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.MessageRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.security.access.AccessDeniedException; @@ -28,16 +26,13 @@ public class ChatService { private final ConversationRepository conversationRepository; private final MessageRepository messageRepository; private final UserRepository userRepository; - private final CustomerRepository customerRepository; public ChatService(ConversationRepository conversationRepository, MessageRepository messageRepository, - UserRepository userRepository, - CustomerRepository customerRepository) { + UserRepository userRepository) { this.conversationRepository = conversationRepository; this.messageRepository = messageRepository; this.userRepository = userRepository; - this.customerRepository = customerRepository; } @Transactional @@ -49,11 +44,8 @@ public class ChatService { throw new AccessDeniedException("Only customers can start new conversations"); } - Customer customer = customerRepository.findByUserId(userId) - .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); - Conversation conversation = new Conversation(); - conversation.setCustomerId(customer.getCustomerId()); + conversation.setCustomerId(userId); conversation.setStatus(Conversation.ConversationStatus.OPEN); conversation.setMode(Conversation.ConversationMode.AUTOMATED); conversation = conversationRepository.save(conversation); @@ -72,9 +64,7 @@ public class ChatService { List conversations; if (role == User.Role.CUSTOMER) { - Customer customer = customerRepository.findByUserId(userId) - .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); - conversations = conversationRepository.findByCustomerId(customer.getCustomerId()); + conversations = conversationRepository.findByCustomerId(userId); } else if (role == User.Role.STAFF) { List assignedToMe = conversationRepository.findByStaffId(userId); List unassigned = conversationRepository.findByStaffIdIsNull(); @@ -225,9 +215,7 @@ public class ChatService { } if (role == User.Role.CUSTOMER) { - Customer customer = customerRepository.findByUserId(userId) - .orElseThrow(() -> new ResourceNotFoundException("Customer record not found for user")); - return conversation.getCustomerId().equals(customer.getCustomerId()); + return conversation.getCustomerId().equals(userId); } if (role == User.Role.STAFF) { diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java b/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java deleted file mode 100644 index fa424737..00000000 --- a/backend/src/main/java/com/petshop/backend/service/CustomerPetService.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.customerpet.CustomerPetRequest; -import com.petshop.backend.dto.customerpet.CustomerPetResponse; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.CustomerPet; -import com.petshop.backend.exception.ResourceNotFoundException; -import com.petshop.backend.repository.CustomerPetRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.UserRepository; -import com.petshop.backend.util.AuthenticationHelper; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -@Service -public class CustomerPetService { - - private final CustomerPetRepository customerPetRepository; - private final CustomerRepository customerRepository; - private final UserRepository userRepository; - private final CatalogImageStorageService catalogImageStorageService; - - public CustomerPetService(CustomerPetRepository customerPetRepository, - CustomerRepository customerRepository, - UserRepository userRepository, - CatalogImageStorageService catalogImageStorageService) { - this.customerPetRepository = customerPetRepository; - this.customerRepository = customerRepository; - this.userRepository = userRepository; - this.catalogImageStorageService = catalogImageStorageService; - } - - @Transactional(readOnly = true) - public List getMyPets() { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - - return customerPetRepository.findByCustomerCustomerIdOrderByCreatedAtDesc(customer.getCustomerId()) - .stream() - .map(this::mapToResponse) - .collect(Collectors.toList()); - } - - @Transactional - public CustomerPetResponse createPet(CustomerPetRequest request) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - - CustomerPet pet = new CustomerPet(); - pet.setCustomer(customer); - pet.setPetName(request.getPetName()); - pet.setSpecies(request.getSpecies()); - pet.setBreed(request.getBreed()); - - pet = customerPetRepository.save(pet); - - return mapToResponse(pet); - } - - @Transactional - public CustomerPetResponse updatePet(Long id, CustomerPetRequest request) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()) - .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); - - pet.setPetName(request.getPetName()); - pet.setSpecies(request.getSpecies()); - pet.setBreed(request.getBreed()); - - pet = customerPetRepository.save(pet); - - return mapToResponse(pet); - } - - @Transactional - public void deletePet(Long id) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); - deleteStoredImageIfPresent(pet.getImageUrl()); - - customerPetRepository.delete(pet); - } - - @Transactional - public CustomerPetResponse uploadImage(Long id, MultipartFile file) throws IOException { - validateImageFile(file); - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); - deleteStoredImageIfPresent(pet.getImageUrl()); - pet.setImageUrl(catalogImageStorageService.storePetImage(file)); - - return mapToResponse(customerPetRepository.save(pet)); - } - - @Transactional - public CustomerPetResponse deleteImage(Long id) { - Customer customer = AuthenticationHelper.getAuthenticatedCustomer(userRepository, customerRepository); - CustomerPet pet = customerPetRepository.findByCustomerPetIdAndCustomerCustomerId(id, customer.getCustomerId()).orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); - deleteStoredImageIfPresent(pet.getImageUrl()); - pet.setImageUrl(null); - - return mapToResponse(customerPetRepository.save(pet)); - } - - private CustomerPetResponse mapToResponse(CustomerPet pet) { - return new CustomerPetResponse( - pet.getCustomerPetId(), - pet.getCustomer().getCustomerId(), - pet.getPetName(), - pet.getSpecies(), - pet.getBreed(), - pet.getImageUrl() != null && !pet.getImageUrl().isBlank() - ? "/api/v1/my-pets/" + pet.getCustomerPetId() + "/image" - : null, - pet.getCreatedAt(), - pet.getUpdatedAt() - ); - } - - private void validateImageFile(MultipartFile file) { - if (file == null || file.isEmpty()) { - - throw new IllegalArgumentException("Please select an image to upload"); - } - - if (file.getSize() > 5 * 1024 * 1024) { - - throw new IllegalArgumentException("Image file size must be less than 5MB"); - } - - String contentType = file.getContentType(); - - if (contentType == null) { - - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); - } - - String normalized = contentType.toLowerCase(Locale.ROOT); - - if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { - - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); - } - } - - private void deleteStoredImageIfPresent(String storedImagePath) { - if (storedImagePath == null || storedImagePath.isBlank()) { - - return; - } - - try { - catalogImageStorageService.deletePetImage(storedImagePath); - } - - catch (IOException ignored) { - } - } -} diff --git a/backend/src/main/java/com/petshop/backend/service/CustomerService.java b/backend/src/main/java/com/petshop/backend/service/CustomerService.java deleted file mode 100644 index 33c731d1..00000000 --- a/backend/src/main/java/com/petshop/backend/service/CustomerService.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.common.BulkDeleteRequest; -import com.petshop.backend.dto.customer.CustomerRequest; -import com.petshop.backend.dto.customer.CustomerResponse; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.User; -import com.petshop.backend.exception.ResourceNotFoundException; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.UserRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; - -import static org.springframework.http.HttpStatus.CONFLICT; - -@Service -public class CustomerService { - - private static final String TEMP_PASSWORD = "TempPass123!"; - - private final CustomerRepository customerRepository; - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; - - public CustomerService(CustomerRepository customerRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { - this.customerRepository = customerRepository; - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; - } - - public Page getAllCustomers(String query, Pageable pageable) { - Page customers; - if (query != null && !query.trim().isEmpty()) { - customers = customerRepository.searchCustomers(query, pageable); - } else { - customers = customerRepository.findAll(pageable); - } - return customers.map(this::mapToResponse); - } - - public CustomerResponse getCustomerById(Long id) { - Customer customer = customerRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); - return mapToResponse(customer); - } - - @Transactional - public CustomerResponse createCustomer(CustomerRequest request) { - ensureEmailAvailable(request.getEmail(), null); - - Customer customer = new Customer(); - customer.setFirstName(request.getFirstName()); - customer.setLastName(request.getLastName()); - customer.setEmail(request.getEmail()); - - customer = customerRepository.save(customer); - User user = createLinkedUser(customer); - - Customer linkedCustomer = userBusinessLinkageService.ensureLinkedCustomer(user); - syncLinkedUser(linkedCustomer); - return mapToResponse(linkedCustomer); - } - - @Transactional - public CustomerResponse updateCustomer(Long id, CustomerRequest request) { - Customer customer = customerRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); - - ensureEmailAvailable(request.getEmail(), customer.getUserId()); - - customer.setFirstName(request.getFirstName()); - customer.setLastName(request.getLastName()); - customer.setEmail(request.getEmail()); - - customer = customerRepository.save(customer); - syncLinkedUser(customer); - return mapToResponse(customer); - } - - @Transactional - public void deleteCustomer(Long id) { - Customer customer = customerRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + id)); - - if (customer.getUserId() != null && userRepository.existsById(customer.getUserId())) { - userRepository.deleteById(customer.getUserId()); - return; - } - - customerRepository.deleteById(id); - } - - @Transactional - public void bulkDeleteCustomers(BulkDeleteRequest request) { - customerRepository.deleteAllById(request.getIds()); - } - - private CustomerResponse mapToResponse(Customer customer) { - return new CustomerResponse( - customer.getCustomerId(), - customer.getFirstName(), - customer.getLastName(), - customer.getEmail(), - customer.getCreatedAt(), - customer.getUpdatedAt() - ); - } - - private void syncLinkedUser(Customer customer) { - if (customer.getUserId() == null) { - return; - } - userRepository.findById(customer.getUserId()).ifPresent(user -> { - user.setEmail(customer.getEmail()); - user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); - userRepository.save(user); - }); - } - - private User createLinkedUser(Customer customer) { - User user = new User(); - user.setUsername(generateUsername(customer)); - user.setPassword(passwordEncoder.encode(TEMP_PASSWORD)); - user.setEmail(customer.getEmail()); - user.setFullName((customer.getFirstName() + " " + customer.getLastName()).trim()); - user.setPhone(generatePhone(customer)); - user.setRole(User.Role.CUSTOMER); - user.setActive(false); - user.setTokenVersion(0); - return userRepository.save(user); - } - - private String generateUsername(Customer customer) { - return "customer_" + customer.getCustomerId(); - } - - private String generatePhone(Customer customer) { - return String.format("200-000-%04d", customer.getCustomerId()); - } - - private void ensureEmailAvailable(String email, Long currentUserId) { - if (email == null || email.isBlank()) { - return; - } - - userRepository.findByEmail(email).ifPresent(existing -> { - if (currentUserId == null || !existing.getId().equals(currentUserId)) { - throw new ResponseStatusException(CONFLICT, "Email already exists"); - } - }); - } -} diff --git a/backend/src/main/java/com/petshop/backend/service/EmployeeService.java b/backend/src/main/java/com/petshop/backend/service/EmployeeService.java deleted file mode 100644 index baf83bb8..00000000 --- a/backend/src/main/java/com/petshop/backend/service/EmployeeService.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.employee.EmployeeRequest; -import com.petshop.backend.dto.employee.EmployeeResponse; -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.User; -import com.petshop.backend.exception.ResourceNotFoundException; -import com.petshop.backend.repository.EmployeeRepository; -import com.petshop.backend.repository.UserRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; - -import static org.springframework.http.HttpStatus.CONFLICT; - -@Service -public class EmployeeService { - private final EmployeeRepository employeeRepository; - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; - - public EmployeeService(EmployeeRepository employeeRepository, UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService) { - this.employeeRepository = employeeRepository; - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; - } - - public Page getAllEmployees(String query, Pageable pageable) { - Page employees; - if (query != null && !query.trim().isEmpty()) { - employees = employeeRepository.searchEmployees(query, pageable); - } else { - employees = employeeRepository.findAll(pageable); - } - return employees.map(this::mapToResponse); - } - - public EmployeeResponse getEmployeeById(Long id) { - Employee employee = employeeRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); - return mapToResponse(employee); - } - - @Transactional - public EmployeeResponse createEmployee(EmployeeRequest request) { - validateRole(request.getRole()); - if (request.getPassword() == null || request.getPassword().trim().length() < 6) { - throw new IllegalArgumentException("Password must be at least 6 characters"); - } - if (userRepository.findByUsername(request.getUsername()).isPresent()) { - throw new ResponseStatusException(CONFLICT, "Username already exists"); - } - if (request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) { - throw new ResponseStatusException(CONFLICT, "Email already exists"); - } - String phone = trimToNull(request.getPhone()); - if (phone != null && userRepository.findByPhone(phone).isPresent()) { - throw new ResponseStatusException(CONFLICT, "Phone already exists"); - } - - User user = new User(); - user.setUsername(request.getUsername()); - user.setPassword(passwordEncoder.encode(request.getPassword())); - user.setFullName(fullName(request)); - user.setEmail(request.getEmail()); - user.setPhone(phone); - user.setRole(request.getRole()); - user.setActive(request.getActive() != null ? request.getActive() : true); - user = userRepository.save(user); - - Employee employee = userBusinessLinkageService.ensureLinkedEmployee(user); - return mapToResponse(employee, user); - } - - @Transactional - public EmployeeResponse updateEmployee(Long id, EmployeeRequest request) { - Employee employee = employeeRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); - User user = requireLinkedUser(employee); - - validateRole(request.getRole()); - if (!user.getUsername().equals(request.getUsername()) && userRepository.findByUsername(request.getUsername()).isPresent()) { - throw new ResponseStatusException(CONFLICT, "Username already exists"); - } - if (!java.util.Objects.equals(user.getEmail(), request.getEmail()) && request.getEmail() != null && userRepository.findByEmail(request.getEmail()).isPresent()) { - throw new ResponseStatusException(CONFLICT, "Email already exists"); - } - String phone = trimToNull(request.getPhone()); - Long currentUserId = user.getId(); - if (!java.util.Objects.equals(user.getPhone(), phone)) { - userRepository.findByPhone(phone) - .filter(existing -> !existing.getId().equals(currentUserId)) - .ifPresent(existing -> { throw new ResponseStatusException(CONFLICT, "Phone already exists"); }); - } - - user.setUsername(request.getUsername()); - if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { - user.setPassword(passwordEncoder.encode(request.getPassword())); - user.setTokenVersion(user.getTokenVersion() + 1); - } - user.setEmail(request.getEmail()); - user.setPhone(phone); - user.setFullName(fullName(request)); - user.setRole(request.getRole()); - user.setActive(request.getActive() != null ? request.getActive() : true); - user = userRepository.save(user); - - employee = userBusinessLinkageService.ensureLinkedEmployee(user); - return mapToResponse(employee, user); - } - - @Transactional - public void deleteEmployee(Long id) { - Employee employee = employeeRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + id)); - if (employee.getUserId() != null && userRepository.existsById(employee.getUserId())) { - userRepository.deleteById(employee.getUserId()); - return; - } - employeeRepository.deleteById(id); - } - - private EmployeeResponse mapToResponse(Employee employee) { - User user = employee.getUserId() == null ? null : userRepository.findById(employee.getUserId()).orElse(null); - return mapToResponse(employee, user); - } - - private EmployeeResponse mapToResponse(Employee employee, User user) { - EmployeeResponse response = new EmployeeResponse(); - response.setEmployeeId(employee.getEmployeeId()); - response.setUserId(user != null ? user.getId() : employee.getUserId()); - response.setUsername(user != null ? user.getUsername() : null); - response.setFirstName(employee.getFirstName()); - response.setLastName(employee.getLastName()); - response.setFullName(user != null ? user.getFullName() : fullName(employee)); - response.setEmail(user != null ? user.getEmail() : employee.getEmail()); - response.setPhone(user != null ? user.getPhone() : null); - response.setRole(user != null ? user.getRole().name() : normalizeRole(employee.getRole())); - response.setActive(user != null ? user.getActive() : employee.getIsActive()); - response.setCreatedAt(employee.getCreatedAt()); - response.setUpdatedAt(employee.getUpdatedAt()); - return response; - } - - private User requireLinkedUser(Employee employee) { - if (employee.getUserId() == null) { - throw new ResourceNotFoundException("Employee user account not found"); - } - return userRepository.findById(employee.getUserId()) - .orElseThrow(() -> new ResourceNotFoundException("Employee user account not found")); - } - - private void validateRole(User.Role role) { - if (role != User.Role.STAFF && role != User.Role.ADMIN) { - throw new IllegalArgumentException("Employee role must be STAFF or ADMIN"); - } - } - - private String fullName(EmployeeRequest request) { - return (request.getFirstName().trim() + " " + request.getLastName().trim()).trim(); - } - - private String fullName(Employee employee) { - return (employee.getFirstName().trim() + " " + employee.getLastName().trim()).trim(); - } - - private String normalizeRole(String role) { - return role == null ? null : role.trim().toUpperCase(java.util.Locale.ROOT); - } - - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } -} diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 7d038ed4..ef6ab4c5 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -4,16 +4,15 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.pet.PetRequest; import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.entity.Adoption; -import com.petshop.backend.entity.Customer; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; -import com.petshop.backend.repository.CustomerRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.repository.UserRepository; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; @@ -33,14 +32,14 @@ public class PetService { private final PetRepository petRepository; private final AdoptionRepository adoptionRepository; - private final CustomerRepository customerRepository; + private final UserRepository userRepository; private final StoreRepository storeRepository; private final CatalogImageStorageService catalogImageStorageService; - public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CustomerRepository customerRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { + public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) { this.petRepository = petRepository; this.adoptionRepository = adoptionRepository; - this.customerRepository = customerRepository; + this.userRepository = userRepository; this.storeRepository = storeRepository; this.catalogImageStorageService = catalogImageStorageService; } @@ -188,7 +187,7 @@ public class PetService { } return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed") .map(Adoption::getCustomer) - .map(customer -> userId.equals(customer.getUserId())) + .map(customer -> userId.equals(customer.getId())) .orElse(false); } @@ -257,7 +256,7 @@ public class PetService { } private PetResponse mapToResponse(Pet pet) { - Customer customer = pet.getCustomer(); + User owner = pet.getOwner(); StoreLocation store = pet.getStore(); return new PetResponse( pet.getPetId(), @@ -270,8 +269,8 @@ public class PetService { pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null, pet.getCreatedAt(), pet.getUpdatedAt(), - customer != null ? customer.getCustomerId() : null, - customer != null ? customer.getFirstName() + " " + customer.getLastName() : null, + owner != null ? owner.getId() : null, + owner != null ? owner.getFirstName() + " " + owner.getLastName() : null, store != null ? store.getStoreId() : null, store != null ? store.getStoreName() : null ); @@ -280,11 +279,11 @@ public class PetService { private void applyOwnerAndStore(Pet pet, PetRequest request) { if ("owned".equalsIgnoreCase(request.getPetStatus())) { if (request.getCustomerId() != null) { - Customer customer = customerRepository.findById(request.getCustomerId()) + User owner = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); - pet.setCustomer(customer); + pet.setOwner(owner); } else { - pet.setCustomer(null); + pet.setOwner(null); } pet.setStore(null); } else if ("available".equalsIgnoreCase(request.getPetStatus()) || "unadopted".equalsIgnoreCase(request.getPetStatus())) { @@ -295,14 +294,14 @@ public class PetService { } else { pet.setStore(null); } - pet.setCustomer(null); + pet.setOwner(null); } else { if (request.getCustomerId() != null) { - Customer customer = customerRepository.findById(request.getCustomerId()) + User owner = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); - pet.setCustomer(customer); + pet.setOwner(owner); } else { - pet.setCustomer(null); + pet.setOwner(null); } if (request.getStoreId() != null) { StoreLocation store = storeRepository.findById(request.getStoreId()) @@ -318,8 +317,8 @@ public class PetService { if (!"owned".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) { return false; } - Customer customer = pet.getCustomer(); - return customer != null && userId.equals(customer.getUserId()); + User owner = pet.getOwner(); + return owner != null && userId.equals(owner.getId()); } public record ImagePayload(Resource resource, MediaType mediaType) { diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java index 61c067fb..6d3ef880 100644 --- a/backend/src/main/java/com/petshop/backend/service/RefundService.java +++ b/backend/src/main/java/com/petshop/backend/service/RefundService.java @@ -46,13 +46,13 @@ public class RefundService { throw new RuntimeException("Sale has no associated customer"); } - if (customerId != null && !sale.getCustomer().getCustomerId().equals(customerId)) { + if (customerId != null && !sale.getCustomer().getId().equals(customerId)) { throw new RuntimeException("You can only create refunds for your own purchases"); } Refund refund = new Refund(); refund.setSaleId(sale.getSaleId()); - refund.setCustomerId(sale.getCustomer().getCustomerId()); + refund.setCustomerId(sale.getCustomer().getId()); refund.setReason(request.getReason()); refund.setStatus(Refund.RefundStatus.PENDING); diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index b8d5861e..acad5e68 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -24,20 +24,14 @@ public class SaleService { private final ProductRepository productRepository; private final StoreRepository storeRepository; private final InventoryRepository inventoryRepository; - private final EmployeeRepository employeeRepository; - private final EmployeeStoreRepository employeeStoreRepository; private final UserRepository userRepository; - private final CustomerRepository customerRepository; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, UserRepository userRepository, CustomerRepository customerRepository) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; this.inventoryRepository = inventoryRepository; - this.employeeRepository = employeeRepository; - this.employeeStoreRepository = employeeStoreRepository; this.userRepository = userRepository; - this.customerRepository = customerRepository; } @Transactional(readOnly = true) @@ -60,18 +54,16 @@ public class SaleService { @Transactional public SaleResponse createSale(SaleRequest request) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - Employee employee = AuthenticationHelper.getAuthenticatedEmployee(userRepository, employeeRepository); - Long employeeStoreId = employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()) - .orElseThrow(() -> new BusinessException("Authenticated staff member is not assigned to a store")) - .getStore() - .getStoreId(); + User employee = AuthenticationHelper.getAuthenticatedUser(userRepository); StoreLocation store = storeRepository.findById(request.getStoreId()) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); - if (user.getRole() == User.Role.STAFF && !employeeStoreId.equals(store.getStoreId())) { - throw new BusinessException("Staff can only create sales for their assigned store"); + if (employee.getRole() == User.Role.STAFF) { + Long assignedStoreId = employee.getPrimaryStore() != null ? employee.getPrimaryStore().getStoreId() : null; + if (!store.getStoreId().equals(assignedStoreId)) { + throw new BusinessException("Staff can only create sales for their assigned store"); + } } Sale sale = new Sale(); @@ -82,7 +74,7 @@ public class SaleService { sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); if (request.getCustomerId() != null) { - Customer customer = customerRepository.findById(request.getCustomerId()) + User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); sale.setCustomer(customer); } @@ -185,7 +177,7 @@ public class SaleService { SaleResponse response = new SaleResponse(); response.setSaleId(sale.getSaleId()); response.setSaleDate(sale.getSaleDate()); - response.setEmployeeId(sale.getEmployee().getEmployeeId()); + response.setEmployeeId(sale.getEmployee().getId()); response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName()); if (sale.getStore() != null) { diff --git a/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java b/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java deleted file mode 100644 index 31cc18d5..00000000 --- a/backend/src/main/java/com/petshop/backend/service/StoreAssignmentService.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.EmployeeStore; -import com.petshop.backend.entity.StoreLocation; -import com.petshop.backend.exception.ResourceNotFoundException; -import com.petshop.backend.repository.EmployeeStoreRepository; -import com.petshop.backend.repository.StoreRepository; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -public class StoreAssignmentService { - - private final EmployeeStoreRepository employeeStoreRepository; - private final StoreRepository storeRepository; - - public StoreAssignmentService(EmployeeStoreRepository employeeStoreRepository, StoreRepository storeRepository) { - this.employeeStoreRepository = employeeStoreRepository; - this.storeRepository = storeRepository; - } - - @Transactional - public void assignStoreIfMissing(Employee employee, Long storeId) { - if (employeeStoreRepository.findByEmployeeEmployeeId(employee.getEmployeeId()).isPresent()) { - return; - } - - StoreLocation store = storeRepository.findById(storeId) - .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId)); - - employeeStoreRepository.save(new EmployeeStore(employee, store)); - } -} diff --git a/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java b/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java deleted file mode 100644 index 05751688..00000000 --- a/backend/src/main/java/com/petshop/backend/service/UserBusinessLinkageService.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -public class UserBusinessLinkageService { - - private final EmployeeRepository employeeRepository; - private final CustomerRepository customerRepository; - - @Autowired - public UserBusinessLinkageService(EmployeeRepository employeeRepository, CustomerRepository customerRepository) { - this.employeeRepository = employeeRepository; - this.customerRepository = customerRepository; - } - - @Transactional - public Employee ensureLinkedEmployee(User user) { - if (user.getId() != null) { - var existing = employeeRepository.findByUserId(user.getId()); - if (existing.isPresent()) { - return syncEmployee(existing.get(), user); - } - } - - List emailMatches = employeeRepository.findAllByEmail(user.getEmail()); - - if (emailMatches.size() == 1) { - Employee employee = emailMatches.get(0); - if (employee.getUserId() == null) { - employee.setUserId(user.getId()); - return syncEmployee(employee, user); - } - } - - Employee newEmployee = new Employee(); - newEmployee.setUserId(user.getId()); - newEmployee.setEmail(user.getEmail()); - - String[] nameParts = splitFullName(user.getFullName()); - newEmployee.setFirstName(nameParts[0]); - newEmployee.setLastName(nameParts[1]); - - newEmployee.setIsActive(true); - - if (user.getRole() == User.Role.ADMIN) { - newEmployee.setRole("Manager"); - } else if (user.getRole() == User.Role.STAFF) { - newEmployee.setRole("Staff"); - } else { - newEmployee.setRole("Staff"); - } - - return syncEmployee(newEmployee, user); - } - - @Transactional - public Customer ensureLinkedCustomer(User user) { - if (user.getId() != null) { - var existing = customerRepository.findByUserId(user.getId()); - if (existing.isPresent()) { - return syncCustomer(existing.get(), user); - } - } - - List emailMatches = customerRepository.findAllByEmail(user.getEmail()); - - if (emailMatches.size() == 1) { - Customer customer = emailMatches.get(0); - if (customer.getUserId() == null) { - customer.setUserId(user.getId()); - return syncCustomer(customer, user); - } - } - - Customer newCustomer = new Customer(); - newCustomer.setUserId(user.getId()); - newCustomer.setEmail(user.getEmail()); - - String[] nameParts = splitFullName(user.getFullName()); - newCustomer.setFirstName(nameParts[0]); - newCustomer.setLastName(nameParts[1]); - - return syncCustomer(newCustomer, user); - } - - @Transactional - public void syncLinkedRecords(User user) { - if (user.getRole() == User.Role.CUSTOMER) { - ensureLinkedCustomer(user); - return; - } - ensureLinkedEmployee(user); - } - - private Employee syncEmployee(Employee employee, User user) { - employee.setUserId(user.getId()); - employee.setEmail(user.getEmail()); - String[] nameParts = splitFullName(user.getFullName()); - employee.setFirstName(nameParts[0]); - employee.setLastName(nameParts[1]); - if (user.getRole() == User.Role.ADMIN) { - employee.setRole("Manager"); - } else { - employee.setRole("Staff"); - } - return employeeRepository.save(employee); - } - - private Customer syncCustomer(Customer customer, User user) { - customer.setUserId(user.getId()); - customer.setEmail(user.getEmail()); - String[] nameParts = splitFullName(user.getFullName()); - customer.setFirstName(nameParts[0]); - customer.setLastName(nameParts[1]); - return customerRepository.save(customer); - } - - private String[] splitFullName(String fullName) { - if (fullName == null || fullName.trim().isEmpty()) { - return new String[]{"System", "User"}; - } - - String trimmed = fullName.trim(); - int spaceIndex = trimmed.indexOf(' '); - - if (spaceIndex == -1) { - return new String[]{trimmed, "User"}; - } - - String firstName = trimmed.substring(0, spaceIndex).trim(); - String lastName = trimmed.substring(spaceIndex + 1).trim(); - - if (firstName.isEmpty()) { - firstName = "System"; - } - if (lastName.isEmpty()) { - lastName = "User"; - } - - return new String[]{firstName, lastName}; - } -} diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index e3a17c5c..a6661cf1 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -26,13 +26,11 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final UserBusinessLinkageService userBusinessLinkageService; private final StoreRepository storeRepository; - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, StoreRepository storeRepository) { + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; - this.userBusinessLinkageService = userBusinessLinkageService; this.storeRepository = storeRepository; } @@ -79,8 +77,6 @@ public class UserService { user = userRepository.save(user); - userBusinessLinkageService.syncLinkedRecords(user); - return mapToResponse(user); } @@ -117,7 +113,6 @@ public class UserService { } user = userRepository.save(user); - userBusinessLinkageService.syncLinkedRecords(user); return mapToResponse(user); } diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java index b1ab33a1..66c7418f 100644 --- a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -1,10 +1,6 @@ package com.petshop.backend.util; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Employee; import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.AppPrincipal; import org.springframework.security.core.Authentication; @@ -47,16 +43,4 @@ public class AuthenticationHelper { return userRepository.findByUsername(username) .orElseThrow(() -> new RuntimeException("User not found: " + username)); } - - public static Employee getAuthenticatedEmployee(UserRepository userRepository, EmployeeRepository employeeRepository) { - User user = getAuthenticatedUser(userRepository); - return employeeRepository.findByUserId(user.getId()) - .orElseThrow(() -> new RuntimeException("Employee record not found for user: " + user.getUsername())); - } - - public static Customer getAuthenticatedCustomer(UserRepository userRepository, CustomerRepository customerRepository) { - User user = getAuthenticatedUser(userRepository); - return customerRepository.findByUserId(user.getId()) - .orElseThrow(() -> new RuntimeException("Customer record not found for user: " + user.getUsername())); - } } From 0482af966e343a027256abb3609bea4a8ad318d6 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:22:26 -0600 Subject: [PATCH 083/137] simplify appointment to single pet --- .../repository/AppointmentRepository.java | 4 +- .../backend/service/AppointmentService.java | 45 +++++-------------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index f4cdc564..00edebf9 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -21,7 +21,7 @@ public interface AppointmentRepository extends JpaRepository @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.store.storeId = :storeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); - @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE " + + @Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE " + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + @@ -30,7 +30,7 @@ public interface AppointmentRepository extends JpaRepository Page findByCustomerId(Long customerId, Pageable pageable); - @Query("SELECT DISTINCT a FROM Appointment a LEFT JOIN a.pets p WHERE a.customer.id = :customerId AND (" + + @Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE a.customer.id = :customerId AND (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index d3a8594e..d7a65149 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -24,9 +24,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; @Service @@ -94,13 +92,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); - - if (!hasPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); - } - - Set pets = fetchPets(request.getPetIds()); + Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null; User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); validateStoreAccess(store.getStoreId(), authenticatedUser); @@ -113,7 +105,7 @@ public class AppointmentService { appointment.setAppointmentDate(request.getAppointmentDate()); appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); - appointment.setPets(pets); + appointment.setPet(pet); appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); @@ -138,13 +130,7 @@ public class AppointmentService { com.petshop.backend.entity.Service service = serviceRepository.findById(request.getServiceId()) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + request.getServiceId())); - boolean hasPetIds = request.getPetIds() != null && !request.getPetIds().isEmpty(); - - if (!hasPetIds) { - throw new IllegalArgumentException("Please specify at least one pet."); - } - - Set pets = fetchPets(request.getPetIds()); + Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null; User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId()); validateStoreAccess(store.getStoreId(), authenticatedUser); @@ -156,7 +142,7 @@ public class AppointmentService { appointment.setAppointmentDate(request.getAppointmentDate()); appointment.setAppointmentTime(request.getAppointmentTime()); appointment.setAppointmentStatus(request.getAppointmentStatus()); - appointment.setPets(pets); + appointment.setPet(pet); appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); @@ -227,24 +213,13 @@ public class AppointmentService { } } - private Set fetchPets(List petIds) { - Set pets = new HashSet<>(); - for (Long petId : petIds) { - Pet pet = petRepository.findById(petId) - .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); - pets.add(pet); - } - return pets; + private Pet fetchPet(Long petId) { + return petRepository.findById(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); } private AppointmentResponse mapToResponse(Appointment appointment) { - List petNames = appointment.getPets().stream() - .map(Pet::getPetName) - .collect(Collectors.toList()); - - List petIds = appointment.getPets().stream() - .map(Pet::getPetId) - .collect(Collectors.toList()); + Pet pet = appointment.getPet(); AppointmentResponse response = new AppointmentResponse(); response.setAppointmentId(appointment.getAppointmentId()); @@ -259,8 +234,8 @@ public class AppointmentService { response.setAppointmentStatus(appointment.getAppointmentStatus()); response.setEmployeeId(appointment.getEmployee().getId()); response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); - response.setPetNames(petNames); - response.setPetIds(petIds); + response.setPetName(pet != null ? pet.getPetName() : null); + response.setPetId(pet != null ? pet.getPetId() : null); response.setCreatedAt(appointment.getCreatedAt()); response.setUpdatedAt(appointment.getUpdatedAt()); From a74e2ac0efc1b611dfc53b87f29d633c3371f694 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:24:23 -0600 Subject: [PATCH 084/137] add store dimension to inventory --- .../dto/inventory/InventoryRequest.java | 16 +++++++++-- .../dto/inventory/InventoryResponse.java | 28 +++++++++++++++++-- .../com/petshop/backend/entity/Inventory.java | 13 +++++++++ .../repository/InventoryRepository.java | 5 ++-- .../backend/service/InventoryService.java | 21 +++++++++++++- 5 files changed, 75 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java index 2dd953c6..7ad02d3a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryRequest.java @@ -12,6 +12,8 @@ public class InventoryRequest { @PositiveOrZero(message = "Quantity must be zero or positive") private Integer quantity; + private Long storeId; + public Long getProdId() { return prodId; } @@ -28,18 +30,27 @@ public class InventoryRequest { this.quantity = quantity; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InventoryRequest that = (InventoryRequest) o; return Objects.equals(prodId, that.prodId) && - Objects.equals(quantity, that.quantity); + Objects.equals(quantity, that.quantity) && + Objects.equals(storeId, that.storeId); } @Override public int hashCode() { - return Objects.hash(prodId, quantity); + return Objects.hash(prodId, quantity, storeId); } @Override @@ -47,6 +58,7 @@ public class InventoryRequest { return "InventoryRequest{" + "prodId=" + prodId + ", quantity=" + quantity + + ", storeId=" + storeId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java index 710dcbf4..5879a554 100644 --- a/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/inventory/InventoryResponse.java @@ -8,6 +8,8 @@ public class InventoryResponse { private Long prodId; private String productName; private String categoryName; + private Long storeId; + private String storeName; private Integer quantity; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -15,11 +17,13 @@ public class InventoryResponse { public InventoryResponse() { } - public InventoryResponse(Long inventoryId, Long prodId, String productName, String categoryName, Integer quantity, LocalDateTime createdAt, LocalDateTime updatedAt) { + public InventoryResponse(Long inventoryId, Long prodId, String productName, String categoryName, Long storeId, String storeName, Integer quantity, LocalDateTime createdAt, LocalDateTime updatedAt) { this.inventoryId = inventoryId; this.prodId = prodId; this.productName = productName; this.categoryName = categoryName; + this.storeId = storeId; + this.storeName = storeName; this.quantity = quantity; this.createdAt = createdAt; this.updatedAt = updatedAt; @@ -57,6 +61,22 @@ public class InventoryResponse { this.categoryName = categoryName; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public Integer getQuantity() { return quantity; } @@ -86,12 +106,12 @@ public class InventoryResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InventoryResponse that = (InventoryResponse) o; - return Objects.equals(inventoryId, that.inventoryId) && Objects.equals(prodId, that.prodId) && Objects.equals(productName, that.productName) && Objects.equals(categoryName, that.categoryName) && Objects.equals(quantity, that.quantity) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(inventoryId, that.inventoryId) && Objects.equals(prodId, that.prodId) && Objects.equals(productName, that.productName) && Objects.equals(categoryName, that.categoryName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(quantity, that.quantity) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(inventoryId, prodId, productName, categoryName, quantity, createdAt, updatedAt); + return Objects.hash(inventoryId, prodId, productName, categoryName, storeId, storeName, quantity, createdAt, updatedAt); } @Override @@ -101,6 +121,8 @@ public class InventoryResponse { ", prodId=" + prodId + ", productName='" + productName + '\'' + ", categoryName='" + categoryName + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + ", quantity=" + quantity + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + diff --git a/backend/src/main/java/com/petshop/backend/entity/Inventory.java b/backend/src/main/java/com/petshop/backend/entity/Inventory.java index 07b93501..ac859c64 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Inventory.java +++ b/backend/src/main/java/com/petshop/backend/entity/Inventory.java @@ -19,6 +19,10 @@ public class Inventory { @JoinColumn(name = "prodId", nullable = false) private Product product; + @ManyToOne + @JoinColumn(name = "storeId") + private StoreLocation store; + @Column(nullable = false) private Integer quantity = 0; @@ -57,6 +61,14 @@ public class Inventory { this.product = product; } + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + public Integer getQuantity() { return quantity; } @@ -99,6 +111,7 @@ public class Inventory { return "Inventory{" + "inventoryId=" + inventoryId + ", product=" + product + + ", store=" + store + ", quantity=" + quantity + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + diff --git a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java index 0e9d358c..69ff16ed 100644 --- a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java @@ -16,8 +16,9 @@ public interface InventoryRepository extends JpaRepository { @Query("SELECT i FROM Inventory i WHERE i.product.prodId = :productId") Optional findByProductId(@Param("productId") Long productId); - @Query("SELECT i FROM Inventory i WHERE " + + @Query("SELECT i FROM Inventory i LEFT JOIN i.store s WHERE " + "LOWER(i.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%'))") + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") Page searchInventory(@Param("q") String query, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index ee63aea7..499e8dd5 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -5,9 +5,11 @@ import com.petshop.backend.dto.inventory.InventoryRequest; import com.petshop.backend.dto.inventory.InventoryResponse; import com.petshop.backend.entity.Inventory; import com.petshop.backend.entity.Product; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.InventoryRepository; import com.petshop.backend.repository.ProductRepository; +import com.petshop.backend.repository.StoreRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -18,10 +20,12 @@ public class InventoryService { private final InventoryRepository inventoryRepository; private final ProductRepository productRepository; + private final StoreRepository storeRepository; - public InventoryService(InventoryRepository inventoryRepository, ProductRepository productRepository) { + public InventoryService(InventoryRepository inventoryRepository, ProductRepository productRepository, StoreRepository storeRepository) { this.inventoryRepository = inventoryRepository; this.productRepository = productRepository; + this.storeRepository = storeRepository; } public Page getAllInventory(String query, Pageable pageable) { @@ -45,8 +49,14 @@ public class InventoryService { Product product = productRepository.findById(request.getProdId()) .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + request.getProdId())); + StoreLocation store = request.getStoreId() != null + ? storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())) + : null; + Inventory inventory = new Inventory(); inventory.setProduct(product); + inventory.setStore(store); inventory.setQuantity(request.getQuantity()); inventory = inventoryRepository.save(inventory); @@ -61,7 +71,13 @@ public class InventoryService { Product product = productRepository.findById(request.getProdId()) .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + request.getProdId())); + StoreLocation store = request.getStoreId() != null + ? storeRepository.findById(request.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())) + : null; + inventory.setProduct(product); + inventory.setStore(store); inventory.setQuantity(request.getQuantity()); inventory = inventoryRepository.save(inventory); @@ -82,11 +98,14 @@ public class InventoryService { } private InventoryResponse mapToResponse(Inventory inventory) { + StoreLocation store = inventory.getStore(); return new InventoryResponse( inventory.getInventoryId(), inventory.getProduct().getProdId(), inventory.getProduct().getProdName(), inventory.getProduct().getCategory().getCategoryName(), + store != null ? store.getStoreId() : null, + store != null ? store.getStoreName() : null, inventory.getQuantity(), inventory.getCreatedAt(), inventory.getUpdatedAt() From f86cf72dd9089f8ebab4163cc12838ebf8538db0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:25:33 -0600 Subject: [PATCH 085/137] add storeLocation imageUrl --- .../petshop/backend/dto/store/StoreRequest.java | 16 ++++++++++++++-- .../petshop/backend/dto/store/StoreResponse.java | 12 +++++++++++- .../petshop/backend/entity/StoreLocation.java | 11 +++++++++++ .../petshop/backend/service/StoreService.java | 3 +++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java b/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java index d6b5fc12..5bb68613 100644 --- a/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/store/StoreRequest.java @@ -18,6 +18,8 @@ public class StoreRequest { @Email(message = "Email must be valid") private String email; + private String imageUrl; + public String getStoreName() { return storeName; } @@ -50,6 +52,14 @@ public class StoreRequest { this.email = email; } + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -58,12 +68,13 @@ public class StoreRequest { return Objects.equals(storeName, that.storeName) && Objects.equals(address, that.address) && Objects.equals(phone, that.phone) && - Objects.equals(email, that.email); + Objects.equals(email, that.email) && + Objects.equals(imageUrl, that.imageUrl); } @Override public int hashCode() { - return Objects.hash(storeName, address, phone, email); + return Objects.hash(storeName, address, phone, email, imageUrl); } @Override @@ -73,6 +84,7 @@ public class StoreRequest { ", address='" + address + '\'' + ", phone='" + phone + '\'' + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java b/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java index e6fd563e..bf5cafd2 100644 --- a/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/store/StoreResponse.java @@ -9,17 +9,19 @@ public class StoreResponse { private String address; private String phone; private String email; + private String imageUrl; private LocalDateTime createdAt; public StoreResponse() { } - public StoreResponse(Long storeId, String storeName, String address, String phone, String email, LocalDateTime createdAt) { + public StoreResponse(Long storeId, String storeName, String address, String phone, String email, String imageUrl, LocalDateTime createdAt) { this.storeId = storeId; this.storeName = storeName; this.address = address; this.phone = phone; this.email = email; + this.imageUrl = imageUrl; this.createdAt = createdAt; } @@ -63,6 +65,14 @@ public class StoreResponse { this.email = email; } + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java index 8c6a9c76..1f79a830 100644 --- a/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java +++ b/backend/src/main/java/com/petshop/backend/entity/StoreLocation.java @@ -28,6 +28,9 @@ public class StoreLocation { @Column(nullable = false, length = 100) private String email; + @Column(length = 500) + private String imageUrl; + @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -89,6 +92,14 @@ public class StoreLocation { this.email = email; } + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/service/StoreService.java b/backend/src/main/java/com/petshop/backend/service/StoreService.java index 5d2c9ce3..ff9b6581 100644 --- a/backend/src/main/java/com/petshop/backend/service/StoreService.java +++ b/backend/src/main/java/com/petshop/backend/service/StoreService.java @@ -43,6 +43,7 @@ public class StoreService { store.setAddress(request.getAddress()); store.setPhone(request.getPhone()); store.setEmail(request.getEmail()); + store.setImageUrl(request.getImageUrl()); store = storeRepository.save(store); return mapToResponse(store); @@ -57,6 +58,7 @@ public class StoreService { store.setAddress(request.getAddress()); store.setPhone(request.getPhone()); store.setEmail(request.getEmail()); + store.setImageUrl(request.getImageUrl()); store = storeRepository.save(store); return mapToResponse(store); @@ -82,6 +84,7 @@ public class StoreService { store.getAddress(), store.getPhone(), store.getEmail(), + store.getImageUrl(), store.getCreatedAt() ); } From 3f6dc132f477c5c747874a48f096890aa3aea8f6 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:26:42 -0600 Subject: [PATCH 086/137] add purchaseOrder store FK --- .../purchaseorder/PurchaseOrderResponse.java | 28 +++++++++++++++++-- .../petshop/backend/entity/PurchaseOrder.java | 15 ++++++++-- .../backend/service/PurchaseOrderService.java | 4 +++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java b/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java index bf575d9d..fbf7d6d9 100644 --- a/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/purchaseorder/PurchaseOrderResponse.java @@ -8,6 +8,8 @@ public class PurchaseOrderResponse { private Long purchaseOrderId; private Long supId; private String supplierName; + private Long storeId; + private String storeName; private LocalDate orderDate; private String status; private LocalDateTime createdAt; @@ -16,10 +18,12 @@ public class PurchaseOrderResponse { public PurchaseOrderResponse() { } - public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, Long storeId, String storeName, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { this.purchaseOrderId = purchaseOrderId; this.supId = supId; this.supplierName = supplierName; + this.storeId = storeId; + this.storeName = storeName; this.orderDate = orderDate; this.status = status; this.createdAt = createdAt; @@ -50,6 +54,22 @@ public class PurchaseOrderResponse { this.supplierName = supplierName; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public LocalDate getOrderDate() { return orderDate; } @@ -87,12 +107,12 @@ public class PurchaseOrderResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PurchaseOrderResponse that = (PurchaseOrderResponse) o; - return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(status, that.status) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(status, that.status) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(purchaseOrderId, supId, supplierName, orderDate, status, createdAt, updatedAt); + return Objects.hash(purchaseOrderId, supId, supplierName, storeId, storeName, orderDate, status, createdAt, updatedAt); } @Override @@ -101,6 +121,8 @@ public class PurchaseOrderResponse { "purchaseOrderId=" + purchaseOrderId + ", supId=" + supId + ", supplierName='" + supplierName + '\'' + + ", storeId=" + storeId + + ", storeName='" + storeName + '\'' + ", orderDate=" + orderDate + ", status='" + status + '\'' + ", createdAt=" + createdAt + diff --git a/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java b/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java index 76fc9a9d..aafe3d89 100644 --- a/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java +++ b/backend/src/main/java/com/petshop/backend/entity/PurchaseOrder.java @@ -4,11 +4,8 @@ import jakarta.persistence.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; @Entity @@ -23,6 +20,10 @@ public class PurchaseOrder { @JoinColumn(name = "supId", nullable = false) private Supplier supplier; + @ManyToOne + @JoinColumn(name = "storeId") + private StoreLocation store; + @Column(nullable = false) private LocalDate orderDate; @@ -65,6 +66,14 @@ public class PurchaseOrder { this.supplier = supplier; } + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + public LocalDate getOrderDate() { return orderDate; } diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java index 97286a9c..e9f7e4a5 100644 --- a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -2,6 +2,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse; import com.petshop.backend.entity.PurchaseOrder; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.PurchaseOrderRepository; import org.springframework.data.domain.Page; @@ -34,10 +35,13 @@ public class PurchaseOrderService { } private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) { + StoreLocation store = purchaseOrder.getStore(); return new PurchaseOrderResponse( purchaseOrder.getPurchaseOrderId(), purchaseOrder.getSupplier().getSupId(), purchaseOrder.getSupplier().getSupCompany(), + store != null ? store.getStoreId() : null, + store != null ? store.getStoreName() : null, purchaseOrder.getOrderDate(), purchaseOrder.getStatus(), purchaseOrder.getCreatedAt(), From 969fbdfe8b89423314837845798421279ad828b0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:28:31 -0600 Subject: [PATCH 087/137] add adoption sourceStore FK --- .../backend/dto/adoption/AdoptionRequest.java | 16 +++++++++-- .../dto/adoption/AdoptionResponse.java | 28 +++++++++++++++++-- .../com/petshop/backend/entity/Adoption.java | 12 ++++++++ .../backend/service/AdoptionService.java | 19 ++++++++++++- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java index 9a34dff8..d3700bfc 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -20,6 +20,8 @@ public class AdoptionRequest { private Long employeeId; + private Long sourceStoreId; + public Long getPetId() { return petId; } @@ -60,6 +62,14 @@ public class AdoptionRequest { this.employeeId = employeeId; } + public Long getSourceStoreId() { + return sourceStoreId; + } + + public void setSourceStoreId(Long sourceStoreId) { + this.sourceStoreId = sourceStoreId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -69,12 +79,13 @@ public class AdoptionRequest { Objects.equals(customerId, that.customerId) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && - Objects.equals(employeeId, that.employeeId); + Objects.equals(employeeId, that.employeeId) && + Objects.equals(sourceStoreId, that.sourceStoreId); } @Override public int hashCode() { - return Objects.hash(petId, customerId, adoptionDate, adoptionStatus, employeeId); + return Objects.hash(petId, customerId, adoptionDate, adoptionStatus, employeeId, sourceStoreId); } @Override @@ -85,6 +96,7 @@ public class AdoptionRequest { ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + ", employeeId=" + employeeId + + ", sourceStoreId=" + sourceStoreId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java index 43128d23..7e831213 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionResponse.java @@ -13,6 +13,8 @@ public class AdoptionResponse { private String customerName; private Long employeeId; private String employeeName; + private Long sourceStoreId; + private String sourceStoreName; private LocalDate adoptionDate; private String adoptionStatus; private BigDecimal adoptionFee; @@ -22,7 +24,7 @@ public class AdoptionResponse { public AdoptionResponse() { } - public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, Long employeeId, String employeeName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { + public AdoptionResponse(Long adoptionId, Long petId, String petName, Long customerId, String customerName, Long employeeId, String employeeName, Long sourceStoreId, String sourceStoreName, LocalDate adoptionDate, String adoptionStatus, BigDecimal adoptionFee, LocalDateTime createdAt, LocalDateTime updatedAt) { this.adoptionId = adoptionId; this.petId = petId; this.petName = petName; @@ -30,6 +32,8 @@ public class AdoptionResponse { this.customerName = customerName; this.employeeId = employeeId; this.employeeName = employeeName; + this.sourceStoreId = sourceStoreId; + this.sourceStoreName = sourceStoreName; this.adoptionDate = adoptionDate; this.adoptionStatus = adoptionStatus; this.adoptionFee = adoptionFee; @@ -93,6 +97,22 @@ public class AdoptionResponse { this.employeeName = employeeName; } + public Long getSourceStoreId() { + return sourceStoreId; + } + + public void setSourceStoreId(Long sourceStoreId) { + this.sourceStoreId = sourceStoreId; + } + + public String getSourceStoreName() { + return sourceStoreName; + } + + public void setSourceStoreName(String sourceStoreName) { + this.sourceStoreName = sourceStoreName; + } + public LocalDate getAdoptionDate() { return adoptionDate; } @@ -138,12 +158,12 @@ public class AdoptionResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AdoptionResponse that = (AdoptionResponse) o; - return Objects.equals(adoptionId, that.adoptionId) && Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && Objects.equals(adoptionFee, that.adoptionFee) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(adoptionId, that.adoptionId) && Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(sourceStoreId, that.sourceStoreId) && Objects.equals(sourceStoreName, that.sourceStoreName) && Objects.equals(adoptionDate, that.adoptionDate) && Objects.equals(adoptionStatus, that.adoptionStatus) && Objects.equals(adoptionFee, that.adoptionFee) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(adoptionId, petId, petName, customerId, customerName, adoptionDate, adoptionStatus, adoptionFee, createdAt, updatedAt); + return Objects.hash(adoptionId, petId, petName, customerId, customerName, sourceStoreId, sourceStoreName, adoptionDate, adoptionStatus, adoptionFee, createdAt, updatedAt); } @Override @@ -154,6 +174,8 @@ public class AdoptionResponse { ", petName='" + petName + '\'' + ", customerId=" + customerId + ", customerName='" + customerName + '\'' + + ", sourceStoreId=" + sourceStoreId + + ", sourceStoreName='" + sourceStoreName + '\'' + ", adoptionDate=" + adoptionDate + ", adoptionStatus='" + adoptionStatus + '\'' + ", adoptionFee=" + adoptionFee + diff --git a/backend/src/main/java/com/petshop/backend/entity/Adoption.java b/backend/src/main/java/com/petshop/backend/entity/Adoption.java index 35d4edac..e5dae5aa 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Adoption.java +++ b/backend/src/main/java/com/petshop/backend/entity/Adoption.java @@ -28,6 +28,10 @@ public class Adoption { @JoinColumn(name = "employeeId", nullable = false) private User employee; + @ManyToOne + @JoinColumn(name = "sourceStoreId") + private StoreLocation sourceStore; + @Column(nullable = false) private LocalDate adoptionDate; @@ -77,6 +81,14 @@ public class Adoption { this.employee = employee; } + public StoreLocation getSourceStore() { + return sourceStore; + } + + public void setSourceStore(StoreLocation sourceStore) { + this.sourceStore = sourceStore; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index caadfbb9..10b3fd02 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -5,10 +5,12 @@ import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Pet; +import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.PetRepository; +import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -27,11 +29,13 @@ public class AdoptionService { private final AdoptionRepository adoptionRepository; private final PetRepository petRepository; private final UserRepository userRepository; + private final StoreRepository storeRepository; - public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository) { + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository) { this.adoptionRepository = adoptionRepository; this.petRepository = petRepository; this.userRepository = userRepository; + this.storeRepository = storeRepository; } public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { @@ -73,6 +77,10 @@ public class AdoptionService { User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); User employee = resolveAdoptionEmployee(request.getEmployeeId()); + StoreLocation sourceStore = request.getSourceStoreId() != null + ? storeRepository.findById(request.getSourceStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId())) + : null; String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); validatePetAvailability(pet, null); @@ -80,6 +88,7 @@ public class AdoptionService { adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); + adoption.setSourceStore(sourceStore); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(adoptionStatus); @@ -99,12 +108,17 @@ public class AdoptionService { User customer = userRepository.findById(request.getCustomerId()) .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId())); User employee = resolveAdoptionEmployee(request.getEmployeeId()); + StoreLocation sourceStore = request.getSourceStoreId() != null + ? storeRepository.findById(request.getSourceStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId())) + : null; String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); validatePetAvailability(pet, adoption.getAdoptionId()); adoption.setPet(pet); adoption.setCustomer(customer); adoption.setEmployee(employee); + adoption.setSourceStore(sourceStore); adoption.setAdoptionDate(request.getAdoptionDate()); adoption.setAdoptionStatus(adoptionStatus); @@ -127,6 +141,7 @@ public class AdoptionService { } private AdoptionResponse mapToResponse(Adoption adoption) { + StoreLocation sourceStore = adoption.getSourceStore(); return new AdoptionResponse( adoption.getAdoptionId(), adoption.getPet().getPetId(), @@ -135,6 +150,8 @@ public class AdoptionService { adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), adoption.getEmployee().getId(), adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(), + sourceStore != null ? sourceStore.getStoreId() : null, + sourceStore != null ? sourceStore.getStoreName() : null, adoption.getAdoptionDate(), adoption.getAdoptionStatus(), adoption.getPet().getPetPrice(), From 682bd12873f7577d20dda48b1145b010e5f2176d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:30:11 -0600 Subject: [PATCH 088/137] add coupon cart cartItem entities --- .../java/com/petshop/backend/entity/Cart.java | 155 +++++++++++++++++ .../com/petshop/backend/entity/CartItem.java | 121 +++++++++++++ .../com/petshop/backend/entity/Coupon.java | 162 ++++++++++++++++++ .../repository/CartItemRepository.java | 13 ++ .../backend/repository/CartRepository.java | 16 ++ .../backend/repository/CouponRepository.java | 13 ++ 6 files changed, 480 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/entity/Cart.java create mode 100644 backend/src/main/java/com/petshop/backend/entity/CartItem.java create mode 100644 backend/src/main/java/com/petshop/backend/entity/Coupon.java create mode 100644 backend/src/main/java/com/petshop/backend/repository/CartItemRepository.java create mode 100644 backend/src/main/java/com/petshop/backend/repository/CartRepository.java create mode 100644 backend/src/main/java/com/petshop/backend/repository/CouponRepository.java diff --git a/backend/src/main/java/com/petshop/backend/entity/Cart.java b/backend/src/main/java/com/petshop/backend/entity/Cart.java new file mode 100644 index 00000000..ba0566f8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Cart.java @@ -0,0 +1,155 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "cart") +public class Cart { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long cartId; + + @ManyToOne + @JoinColumn(name = "userId", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "storeId") + private StoreLocation store; + + @ManyToOne + @JoinColumn(name = "couponId") + private Coupon coupon; + + @Column(nullable = false, length = 20) + private String cartStatus = "ACTIVE"; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal subtotalAmount = BigDecimal.ZERO; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal discountAmount = BigDecimal.ZERO; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal totalAmount = BigDecimal.ZERO; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Cart() { + } + + public Long getCartId() { + return cartId; + } + + public void setCartId(Long cartId) { + this.cartId = cartId; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + + public Coupon getCoupon() { + return coupon; + } + + public void setCoupon(Coupon coupon) { + this.coupon = coupon; + } + + public String getCartStatus() { + return cartStatus; + } + + public void setCartStatus(String cartStatus) { + this.cartStatus = cartStatus; + } + + public BigDecimal getSubtotalAmount() { + return subtotalAmount; + } + + public void setSubtotalAmount(BigDecimal subtotalAmount) { + this.subtotalAmount = subtotalAmount; + } + + public BigDecimal getDiscountAmount() { + return discountAmount; + } + + public void setDiscountAmount(BigDecimal discountAmount) { + this.discountAmount = discountAmount; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Cart cart = (Cart) o; + return Objects.equals(cartId, cart.cartId); + } + + @Override + public int hashCode() { + return Objects.hash(cartId); + } + + @Override + public String toString() { + return "Cart{" + + "cartId=" + cartId + + ", cartStatus='" + cartStatus + '\'' + + ", totalAmount=" + totalAmount + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/CartItem.java b/backend/src/main/java/com/petshop/backend/entity/CartItem.java new file mode 100644 index 00000000..1ad4c144 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/CartItem.java @@ -0,0 +1,121 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "cart_item") +public class CartItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long cartItemId; + + @ManyToOne + @JoinColumn(name = "cartId", nullable = false) + private Cart cart; + + @ManyToOne + @JoinColumn(name = "prodId", nullable = false) + private Product product; + + @Column(nullable = false) + private Integer quantity; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal unitPrice; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public CartItem() { + } + + public Long getCartItemId() { + return cartItemId; + } + + public void setCartItemId(Long cartItemId) { + this.cartItemId = cartItemId; + } + + public Cart getCart() { + return cart; + } + + public void setCart(Cart cart) { + this.cart = cart; + } + + public Product getProduct() { + return product; + } + + public void setProduct(Product product) { + this.product = product; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(BigDecimal unitPrice) { + this.unitPrice = unitPrice; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CartItem cartItem = (CartItem) o; + return Objects.equals(cartItemId, cartItem.cartItemId); + } + + @Override + public int hashCode() { + return Objects.hash(cartItemId); + } + + @Override + public String toString() { + return "CartItem{" + + "cartItemId=" + cartItemId + + ", quantity=" + quantity + + ", unitPrice=" + unitPrice + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/entity/Coupon.java b/backend/src/main/java/com/petshop/backend/entity/Coupon.java new file mode 100644 index 00000000..ba234f12 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/entity/Coupon.java @@ -0,0 +1,162 @@ +package com.petshop.backend.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Objects; + +@Entity +@Table(name = "coupon") +public class Coupon { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long couponId; + + @Column(nullable = false, length = 50, unique = true) + private String couponCode; + + @Column(nullable = false, length = 20) + private String discountType; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal discountValue; + + @Column(precision = 10, scale = 2) + private BigDecimal minOrderAmount; + + @Column(nullable = false) + private Boolean active = true; + + private LocalDateTime startsAt; + + private LocalDateTime endsAt; + + private Integer usageLimit; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public Coupon() { + } + + public Long getCouponId() { + return couponId; + } + + public void setCouponId(Long couponId) { + this.couponId = couponId; + } + + public String getCouponCode() { + return couponCode; + } + + public void setCouponCode(String couponCode) { + this.couponCode = couponCode; + } + + public String getDiscountType() { + return discountType; + } + + public void setDiscountType(String discountType) { + this.discountType = discountType; + } + + public BigDecimal getDiscountValue() { + return discountValue; + } + + public void setDiscountValue(BigDecimal discountValue) { + this.discountValue = discountValue; + } + + public BigDecimal getMinOrderAmount() { + return minOrderAmount; + } + + public void setMinOrderAmount(BigDecimal minOrderAmount) { + this.minOrderAmount = minOrderAmount; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public LocalDateTime getStartsAt() { + return startsAt; + } + + public void setStartsAt(LocalDateTime startsAt) { + this.startsAt = startsAt; + } + + public LocalDateTime getEndsAt() { + return endsAt; + } + + public void setEndsAt(LocalDateTime endsAt) { + this.endsAt = endsAt; + } + + public Integer getUsageLimit() { + return usageLimit; + } + + public void setUsageLimit(Integer usageLimit) { + this.usageLimit = usageLimit; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Coupon coupon = (Coupon) o; + return Objects.equals(couponId, coupon.couponId); + } + + @Override + public int hashCode() { + return Objects.hash(couponId); + } + + @Override + public String toString() { + return "Coupon{" + + "couponId=" + couponId + + ", couponCode='" + couponCode + '\'' + + ", discountType='" + discountType + '\'' + + ", discountValue=" + discountValue + + ", active=" + active + + '}'; + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/CartItemRepository.java b/backend/src/main/java/com/petshop/backend/repository/CartItemRepository.java new file mode 100644 index 00000000..ffcdc166 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CartItemRepository.java @@ -0,0 +1,13 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.CartItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CartItemRepository extends JpaRepository { + + List findByCartCartId(Long cartId); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/CartRepository.java b/backend/src/main/java/com/petshop/backend/repository/CartRepository.java new file mode 100644 index 00000000..3f6a974a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CartRepository.java @@ -0,0 +1,16 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Cart; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CartRepository extends JpaRepository { + + List findByUserId(Long userId); + + Optional findByUserIdAndCartStatus(Long userId, String cartStatus); +} diff --git a/backend/src/main/java/com/petshop/backend/repository/CouponRepository.java b/backend/src/main/java/com/petshop/backend/repository/CouponRepository.java new file mode 100644 index 00000000..64870204 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/repository/CouponRepository.java @@ -0,0 +1,13 @@ +package com.petshop.backend.repository; + +import com.petshop.backend.entity.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CouponRepository extends JpaRepository { + + Optional findByCouponCode(String couponCode); +} From a4ed9a7afce6c419e22b059d1a6083f31876af22 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:32:05 -0600 Subject: [PATCH 089/137] add sale channel coupon cart columns --- .../petshop/backend/dto/sale/SaleRequest.java | 40 +++++++++- .../backend/dto/sale/SaleResponse.java | 78 ++++++++++++++---- .../java/com/petshop/backend/entity/Sale.java | 79 +++++++++++++++++++ .../petshop/backend/service/SaleService.java | 30 ++++++- 4 files changed, 209 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java index 9c7102f4..081ab05d 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleRequest.java @@ -22,6 +22,12 @@ public class SaleRequest { private Long customerId; + private String channel; + + private Long couponId; + + private Long cartId; + public Long getStoreId() { return storeId; } @@ -70,6 +76,30 @@ public class SaleRequest { this.customerId = customerId; } + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public Long getCouponId() { + return couponId; + } + + public void setCouponId(Long couponId) { + this.couponId = couponId; + } + + public Long getCartId() { + return cartId; + } + + public void setCartId(Long cartId) { + this.cartId = cartId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -80,12 +110,15 @@ public class SaleRequest { Objects.equals(items, that.items) && Objects.equals(isRefund, that.isRefund) && Objects.equals(originalSaleId, that.originalSaleId) && - Objects.equals(customerId, that.customerId); + Objects.equals(customerId, that.customerId) && + Objects.equals(channel, that.channel) && + Objects.equals(couponId, that.couponId) && + Objects.equals(cartId, that.cartId); } @Override public int hashCode() { - return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId); + return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId); } @Override @@ -97,6 +130,9 @@ public class SaleRequest { ", isRefund=" + isRefund + ", originalSaleId=" + originalSaleId + ", customerId=" + customerId + + ", channel='" + channel + '\'' + + ", couponId=" + couponId + + ", cartId=" + cartId + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java index 969b28d3..6523505c 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleResponse.java @@ -13,6 +13,13 @@ public class SaleResponse { private Long storeId; private String storeName; private BigDecimal totalAmount; + private BigDecimal subtotalAmount; + private BigDecimal couponDiscountAmount; + private BigDecimal employeeDiscountAmount; + private Integer pointsEarned; + private String channel; + private Long couponId; + private Long cartId; private String paymentMethod; private Boolean isRefund; private Long originalSaleId; @@ -22,21 +29,6 @@ public class SaleResponse { public SaleResponse() { } - public SaleResponse(Long saleId, LocalDateTime saleDate, Long employeeId, String employeeName, Long storeId, String storeName, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Long originalSaleId, List items, LocalDateTime createdAt) { - this.saleId = saleId; - this.saleDate = saleDate; - this.employeeId = employeeId; - this.employeeName = employeeName; - this.storeId = storeId; - this.storeName = storeName; - this.totalAmount = totalAmount; - this.paymentMethod = paymentMethod; - this.isRefund = isRefund; - this.originalSaleId = originalSaleId; - this.items = items; - this.createdAt = createdAt; - } - public Long getSaleId() { return saleId; } @@ -93,6 +85,62 @@ public class SaleResponse { this.totalAmount = totalAmount; } + public BigDecimal getSubtotalAmount() { + return subtotalAmount; + } + + public void setSubtotalAmount(BigDecimal subtotalAmount) { + this.subtotalAmount = subtotalAmount; + } + + public BigDecimal getCouponDiscountAmount() { + return couponDiscountAmount; + } + + public void setCouponDiscountAmount(BigDecimal couponDiscountAmount) { + this.couponDiscountAmount = couponDiscountAmount; + } + + public BigDecimal getEmployeeDiscountAmount() { + return employeeDiscountAmount; + } + + public void setEmployeeDiscountAmount(BigDecimal employeeDiscountAmount) { + this.employeeDiscountAmount = employeeDiscountAmount; + } + + public Integer getPointsEarned() { + return pointsEarned; + } + + public void setPointsEarned(Integer pointsEarned) { + this.pointsEarned = pointsEarned; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public Long getCouponId() { + return couponId; + } + + public void setCouponId(Long couponId) { + this.couponId = couponId; + } + + public Long getCartId() { + return cartId; + } + + public void setCartId(Long cartId) { + this.cartId = cartId; + } + public String getPaymentMethod() { return paymentMethod; } diff --git a/backend/src/main/java/com/petshop/backend/entity/Sale.java b/backend/src/main/java/com/petshop/backend/entity/Sale.java index ee1a9d51..3bf4d8bf 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Sale.java +++ b/backend/src/main/java/com/petshop/backend/entity/Sale.java @@ -46,6 +46,29 @@ public class Sale { @JoinColumn(name = "originalSaleId") private Sale originalSale; + @Column(nullable = false, length = 20) + private String channel = "IN_STORE"; + + @ManyToOne + @JoinColumn(name = "cartId") + private Cart cart; + + @ManyToOne + @JoinColumn(name = "couponId") + private Coupon coupon; + + @Column(precision = 10, scale = 2) + private BigDecimal subtotalAmount; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal couponDiscountAmount = BigDecimal.ZERO; + + @Column(nullable = false, precision = 10, scale = 2) + private BigDecimal employeeDiscountAmount = BigDecimal.ZERO; + + @Column(nullable = false) + private Integer pointsEarned = 0; + @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) private List items = new ArrayList<>(); @@ -132,6 +155,62 @@ public class Sale { this.originalSale = originalSale; } + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public Cart getCart() { + return cart; + } + + public void setCart(Cart cart) { + this.cart = cart; + } + + public Coupon getCoupon() { + return coupon; + } + + public void setCoupon(Coupon coupon) { + this.coupon = coupon; + } + + public BigDecimal getSubtotalAmount() { + return subtotalAmount; + } + + public void setSubtotalAmount(BigDecimal subtotalAmount) { + this.subtotalAmount = subtotalAmount; + } + + public BigDecimal getCouponDiscountAmount() { + return couponDiscountAmount; + } + + public void setCouponDiscountAmount(BigDecimal couponDiscountAmount) { + this.couponDiscountAmount = couponDiscountAmount; + } + + public BigDecimal getEmployeeDiscountAmount() { + return employeeDiscountAmount; + } + + public void setEmployeeDiscountAmount(BigDecimal employeeDiscountAmount) { + this.employeeDiscountAmount = employeeDiscountAmount; + } + + public Integer getPointsEarned() { + return pointsEarned; + } + + public void setPointsEarned(Integer pointsEarned) { + this.pointsEarned = pointsEarned; + } + public List getItems() { return items; } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index acad5e68..643cfc2a 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -25,13 +25,17 @@ public class SaleService { private final StoreRepository storeRepository; private final InventoryRepository inventoryRepository; private final UserRepository userRepository; + private final CouponRepository couponRepository; + private final CartRepository cartRepository; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; this.inventoryRepository = inventoryRepository; this.userRepository = userRepository; + this.couponRepository = couponRepository; + this.cartRepository = cartRepository; } @Transactional(readOnly = true) @@ -72,6 +76,19 @@ public class SaleService { sale.setStore(store); sale.setPaymentMethod(normalizePaymentMethod(request.getPaymentMethod())); sale.setIsRefund(request.getIsRefund() != null ? request.getIsRefund() : false); + sale.setChannel(request.getChannel() != null ? request.getChannel() : "IN_STORE"); + + if (request.getCouponId() != null) { + Coupon coupon = couponRepository.findById(request.getCouponId()) + .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId())); + sale.setCoupon(coupon); + } + + if (request.getCartId() != null) { + Cart cart = cartRepository.findById(request.getCartId()) + .orElseThrow(() -> new ResourceNotFoundException("Cart not found with id: " + request.getCartId())); + sale.setCart(cart); + } if (request.getCustomerId() != null) { User customer = userRepository.findById(request.getCustomerId()) @@ -186,6 +203,17 @@ public class SaleService { } response.setTotalAmount(sale.getTotalAmount()); + response.setSubtotalAmount(sale.getSubtotalAmount()); + response.setCouponDiscountAmount(sale.getCouponDiscountAmount()); + response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount()); + response.setPointsEarned(sale.getPointsEarned()); + response.setChannel(sale.getChannel()); + if (sale.getCoupon() != null) { + response.setCouponId(sale.getCoupon().getCouponId()); + } + if (sale.getCart() != null) { + response.setCartId(sale.getCart().getCartId()); + } response.setPaymentMethod(sale.getPaymentMethod()); response.setIsRefund(sale.getIsRefund()); if (sale.getOriginalSale() != null) { From 31a4356d83f7a93763eec304f149edbf5954754e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:34:03 -0600 Subject: [PATCH 090/137] add service species collection --- .../backend/dto/service/ServiceRequest.java | 18 ++++++++++++++++-- .../backend/dto/service/ServiceResponse.java | 18 +++++++++++++++--- .../com/petshop/backend/entity/Service.java | 15 +++++++++++++++ .../backend/service/ServiceService.java | 7 +++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java index 6b4550ec..c84ac5f7 100644 --- a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java @@ -4,7 +4,9 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.math.BigDecimal; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; public class ServiceRequest { @NotBlank(message = "Service name is required") @@ -19,6 +21,8 @@ public class ServiceRequest { @Positive(message = "Duration must be positive") private Integer serviceDuration; + private Set species = new HashSet<>(); + public String getServiceName() { return serviceName; } @@ -51,6 +55,14 @@ public class ServiceRequest { this.serviceDuration = serviceDuration; } + public Set getSpecies() { + return species; + } + + public void setSpecies(Set species) { + this.species = species; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -59,12 +71,13 @@ public class ServiceRequest { return Objects.equals(serviceName, that.serviceName) && Objects.equals(serviceDesc, that.serviceDesc) && Objects.equals(servicePrice, that.servicePrice) && - Objects.equals(serviceDuration, that.serviceDuration); + Objects.equals(serviceDuration, that.serviceDuration) && + Objects.equals(species, that.species); } @Override public int hashCode() { - return Objects.hash(serviceName, serviceDesc, servicePrice, serviceDuration); + return Objects.hash(serviceName, serviceDesc, servicePrice, serviceDuration, species); } @Override @@ -74,6 +87,7 @@ public class ServiceRequest { ", serviceDesc='" + serviceDesc + '\'' + ", servicePrice=" + servicePrice + ", serviceDuration=" + serviceDuration + + ", species=" + species + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java index 53a2be5b..4f0300ab 100644 --- a/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceResponse.java @@ -3,6 +3,7 @@ package com.petshop.backend.dto.service; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Objects; +import java.util.Set; public class ServiceResponse { private Long serviceId; @@ -10,18 +11,20 @@ public class ServiceResponse { private String serviceDesc; private BigDecimal servicePrice; private Integer serviceDuration; + private Set species; private LocalDateTime createdAt; private LocalDateTime updatedAt; public ServiceResponse() { } - public ServiceResponse(Long serviceId, String serviceName, String serviceDesc, BigDecimal servicePrice, Integer serviceDuration, LocalDateTime createdAt, LocalDateTime updatedAt) { + public ServiceResponse(Long serviceId, String serviceName, String serviceDesc, BigDecimal servicePrice, Integer serviceDuration, Set species, LocalDateTime createdAt, LocalDateTime updatedAt) { this.serviceId = serviceId; this.serviceName = serviceName; this.serviceDesc = serviceDesc; this.servicePrice = servicePrice; this.serviceDuration = serviceDuration; + this.species = species; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -66,6 +69,14 @@ public class ServiceResponse { this.serviceDuration = serviceDuration; } + public Set getSpecies() { + return species; + } + + public void setSpecies(Set species) { + this.species = species; + } + public LocalDateTime getCreatedAt() { return createdAt; } @@ -87,12 +98,12 @@ public class ServiceResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ServiceResponse that = (ServiceResponse) o; - return Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(serviceDesc, that.serviceDesc) && Objects.equals(servicePrice, that.servicePrice) && Objects.equals(serviceDuration, that.serviceDuration) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(serviceDesc, that.serviceDesc) && Objects.equals(servicePrice, that.servicePrice) && Objects.equals(serviceDuration, that.serviceDuration) && Objects.equals(species, that.species) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(serviceId, serviceName, serviceDesc, servicePrice, serviceDuration, createdAt, updatedAt); + return Objects.hash(serviceId, serviceName, serviceDesc, servicePrice, serviceDuration, species, createdAt, updatedAt); } @Override @@ -103,6 +114,7 @@ public class ServiceResponse { ", serviceDesc='" + serviceDesc + '\'' + ", servicePrice=" + servicePrice + ", serviceDuration=" + serviceDuration + + ", species=" + species + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; diff --git a/backend/src/main/java/com/petshop/backend/entity/Service.java b/backend/src/main/java/com/petshop/backend/entity/Service.java index a73387c8..77223922 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Service.java +++ b/backend/src/main/java/com/petshop/backend/entity/Service.java @@ -6,7 +6,9 @@ import org.hibernate.annotations.UpdateTimestamp; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; @Entity @Table(name = "service") @@ -28,6 +30,11 @@ public class Service { @Column(nullable = false) private Integer serviceDuration; + @ElementCollection + @CollectionTable(name = "service_species", joinColumns = @JoinColumn(name = "serviceId")) + @Column(name = "species", length = 50) + private Set species = new HashSet<>(); + @CreationTimestamp @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @@ -89,6 +96,14 @@ public class Service { this.serviceDuration = serviceDuration; } + public Set getSpecies() { + return species; + } + + public void setSpecies(Set species) { + this.species = species; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/petshop/backend/service/ServiceService.java b/backend/src/main/java/com/petshop/backend/service/ServiceService.java index 5243f101..e2395392 100644 --- a/backend/src/main/java/com/petshop/backend/service/ServiceService.java +++ b/backend/src/main/java/com/petshop/backend/service/ServiceService.java @@ -42,6 +42,9 @@ public class ServiceService { service.setServiceDesc(request.getServiceDesc()); service.setServicePrice(request.getServicePrice()); service.setServiceDuration(request.getServiceDuration()); + if (request.getSpecies() != null) { + service.setSpecies(request.getSpecies()); + } service = serviceRepository.save(service); return mapToResponse(service); @@ -56,6 +59,9 @@ public class ServiceService { service.setServiceDesc(request.getServiceDesc()); service.setServicePrice(request.getServicePrice()); service.setServiceDuration(request.getServiceDuration()); + if (request.getSpecies() != null) { + service.setSpecies(request.getSpecies()); + } service = serviceRepository.save(service); return mapToResponse(service); @@ -81,6 +87,7 @@ public class ServiceService { service.getServiceDesc(), service.getServicePrice(), service.getServiceDuration(), + service.getSpecies(), service.getCreatedAt(), service.getUpdatedAt() ); From 6bde4f4e471d7277e435e72aacccde62e5a1918b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:38:29 -0600 Subject: [PATCH 091/137] add message attachment fields --- .../backend/dto/chat/MessageRequest.java | 43 +++++++++++++++--- .../backend/dto/chat/MessageResponse.java | 40 +++++++++++++++++ .../com/petshop/backend/entity/Message.java | 45 ++++++++++++++++++- .../petshop/backend/service/ChatService.java | 16 +++++-- 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java index cb03d310..cecedfbd 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageRequest.java @@ -1,18 +1,15 @@ package com.petshop.backend.dto.chat; -import jakarta.validation.constraints.NotBlank; - public class MessageRequest { - @NotBlank(message = "Message content is required") private String content; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageRequest() { } - public MessageRequest(String content) { - this.content = content; - } - public String getContent() { return content; } @@ -20,4 +17,36 @@ public class MessageRequest { public void setContent(String content) { this.content = content; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java b/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java index 25cffae5..fba1c7c8 100644 --- a/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/chat/MessageResponse.java @@ -11,6 +11,10 @@ public class MessageResponse { private String content; private LocalDateTime timestamp; private Boolean isRead; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageResponse() { } @@ -32,6 +36,10 @@ public class MessageResponse { response.setContent(message.getContent()); response.setTimestamp(message.getTimestamp()); response.setIsRead(message.getIsRead()); + response.setAttachmentUrl(message.getAttachmentUrl()); + response.setAttachmentName(message.getAttachmentName()); + response.setAttachmentMimeType(message.getAttachmentMimeType()); + response.setAttachmentSizeBytes(message.getAttachmentSizeBytes()); return response; } @@ -82,4 +90,36 @@ public class MessageResponse { public void setIsRead(Boolean isRead) { this.isRead = isRead; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Message.java b/backend/src/main/java/com/petshop/backend/entity/Message.java index 33777bf5..7c7bc498 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Message.java +++ b/backend/src/main/java/com/petshop/backend/entity/Message.java @@ -19,9 +19,20 @@ public class Message { @Column(nullable = false) private Long senderId; - @Column(nullable = false, columnDefinition = "TEXT") + @Column(columnDefinition = "TEXT") private String content; + @Column(length = 255) + private String attachmentUrl; + + @Column(length = 255) + private String attachmentName; + + @Column(length = 100) + private String attachmentMimeType; + + private Long attachmentSizeBytes; + @CreationTimestamp @Column(nullable = false, updatable = false) private LocalDateTime timestamp; @@ -88,4 +99,36 @@ public class Message { public void setIsRead(Boolean isRead) { this.isRead = isRead; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index e39d9c55..e076daed 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -77,7 +77,8 @@ public class ChatService { return conversations.stream() .map(conv -> { List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId()); - String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1); + String lastMessage = last != null && last.getContent() != null ? last.getContent() : ""; return ConversationResponse.fromEntity(conv, lastMessage); }) .collect(Collectors.toList()); @@ -97,7 +98,8 @@ public class ChatService { } List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); - String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1); + String lastMessage = last != null && last.getContent() != null ? last.getContent() : ""; return ConversationResponse.fromEntity(conversation, lastMessage); } @@ -124,6 +126,10 @@ public class ChatService { message.setConversationId(conversationId); message.setSenderId(userId); message.setContent(request.getContent()); + message.setAttachmentUrl(request.getAttachmentUrl()); + message.setAttachmentName(request.getAttachmentName()); + message.setAttachmentMimeType(request.getAttachmentMimeType()); + message.setAttachmentSizeBytes(request.getAttachmentSizeBytes()); message.setIsRead(false); message = messageRepository.save(message); @@ -158,7 +164,8 @@ public class ChatService { conversationRepository.save(conversation); List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); - String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1); + String lastMessage = last != null && last.getContent() != null ? last.getContent() : ""; return ConversationResponse.fromEntity(conversation, lastMessage); } @@ -180,7 +187,8 @@ public class ChatService { conversation = conversationRepository.save(conversation); List messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId); - String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent(); + Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1); + String lastMessage = last != null && last.getContent() != null ? last.getContent() : ""; return ConversationResponse.fromEntity(conversation, lastMessage); } From dac5f8c4a61b2b6a10adaa24ab936a07f2463565 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:39:08 -0600 Subject: [PATCH 092/137] add activityLog store FK --- .../java/com/petshop/backend/entity/ActivityLog.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java index 7c778bc3..7445de57 100644 --- a/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java +++ b/backend/src/main/java/com/petshop/backend/entity/ActivityLog.java @@ -17,6 +17,10 @@ public class ActivityLog { @JoinColumn(name = "userId", nullable = false) private User user; + @ManyToOne + @JoinColumn(name = "storeId") + private StoreLocation store; + @Column(nullable = false, columnDefinition = "TEXT") private String activity; @@ -49,6 +53,14 @@ public class ActivityLog { this.user = user; } + public StoreLocation getStore() { + return store; + } + + public void setStore(StoreLocation store) { + this.store = store; + } + public String getActivity() { return activity; } From 2420453daaf68c4fae23a9416775a8aeca368bed Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:46:27 -0600 Subject: [PATCH 093/137] enable Hibernate validation --- .../backend/RuntimeClasspathValidator.java | 2 +- .../dto/appointment/AppointmentRequest.java | 17 +- .../dto/appointment/AppointmentResponse.java | 35 +- .../petshop/backend/entity/Appointment.java | 22 +- backend/src/main/resources/application.yml | 4 +- .../controller/DropdownControllerTest.java | 201 ----------- .../backend/service/AdoptionServiceTest.java | 150 -------- .../service/AppointmentServiceTest.java | 332 ------------------ .../backend/service/ChatServiceTest.java | 199 ----------- .../backend/service/PetServiceTest.java | 173 --------- 10 files changed, 36 insertions(+), 1099 deletions(-) delete mode 100644 backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java delete mode 100644 backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java delete mode 100644 backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java delete mode 100644 backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java delete mode 100644 backend/src/test/java/com/petshop/backend/service/PetServiceTest.java diff --git a/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java b/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java index ad18d198..e123f66a 100644 --- a/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java +++ b/backend/src/main/java/com/petshop/backend/RuntimeClasspathValidator.java @@ -11,7 +11,7 @@ final class RuntimeClasspathValidator { if (!resourceExists("application.yml")) { throw new IllegalStateException("Backend resources are missing from the runtime classpath. Reimport the Maven project in IntelliJ and run the shared Maven run configuration."); } - if (!resourceExists("db/migration/V1__baseline_schema.sql")) { + if (!resourceExists("db/migration/V1__target_baseline.sql")) { throw new IllegalStateException("Flyway migration files are missing from the runtime classpath. Reimport the Maven project in IntelliJ and run the shared Maven run configuration."); } } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index c60c3fcd..9ddb9ad2 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -3,7 +3,6 @@ package com.petshop.backend.dto.appointment; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalTime; -import java.util.List; import java.util.Objects; public class AppointmentRequest { @@ -25,7 +24,7 @@ public class AppointmentRequest { @NotNull(message = "Appointment status is required") private String appointmentStatus; - private List petIds; + private Long petId; private Long employeeId; @@ -77,12 +76,12 @@ public class AppointmentRequest { this.appointmentStatus = appointmentStatus; } - public List getPetIds() { - return petIds; + public Long getPetId() { + return petId; } - public void setPetIds(List petIds) { - this.petIds = petIds; + public void setPetId(Long petId) { + this.petId = petId; } public Long getEmployeeId() { @@ -104,13 +103,13 @@ public class AppointmentRequest { Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && - Objects.equals(petIds, that.petIds) && + Objects.equals(petId, that.petId) && Objects.equals(employeeId, that.employeeId); } @Override public int hashCode() { - return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petIds, employeeId); + return Objects.hash(customerId, storeId, serviceId, appointmentDate, appointmentTime, appointmentStatus, petId, employeeId); } @Override @@ -122,7 +121,7 @@ public class AppointmentRequest { ", appointmentDate=" + appointmentDate + ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + - ", petIds=" + petIds + + ", petId=" + petId + ", employeeId=" + employeeId + '}'; } diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java index f8e14ac2..655a788c 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentResponse.java @@ -3,7 +3,6 @@ package com.petshop.backend.dto.appointment; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.List; import java.util.Objects; public class AppointmentResponse { @@ -19,15 +18,15 @@ public class AppointmentResponse { private String appointmentStatus; private Long employeeId; private String employeeName; - private List petNames; - private List petIds; + private String petName; + private Long petId; private LocalDateTime createdAt; private LocalDateTime updatedAt; public AppointmentResponse() { } - public AppointmentResponse(Long appointmentId, Long customerId, String customerName, Long storeId, String storeName, Long serviceId, String serviceName, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, List petNames, List petIds, LocalDateTime createdAt, LocalDateTime updatedAt) { + public AppointmentResponse(Long appointmentId, Long customerId, String customerName, Long storeId, String storeName, Long serviceId, String serviceName, LocalDate appointmentDate, LocalTime appointmentTime, String appointmentStatus, String petName, Long petId, LocalDateTime createdAt, LocalDateTime updatedAt) { this.appointmentId = appointmentId; this.customerId = customerId; this.customerName = customerName; @@ -38,8 +37,8 @@ public class AppointmentResponse { this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; - this.petNames = petNames; - this.petIds = petIds; + this.petName = petName; + this.petId = petId; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -140,20 +139,20 @@ public class AppointmentResponse { this.employeeName = employeeName; } - public List getPetNames() { - return petNames; + public String getPetName() { + return petName; } - public void setPetNames(List petNames) { - this.petNames = petNames; + public void setPetName(String petName) { + this.petName = petName; } - public List getPetIds() { - return petIds; + public Long getPetId() { + return petId; } - public void setPetIds(List petIds) { - this.petIds = petIds; + public void setPetId(Long petId) { + this.petId = petId; } public LocalDateTime getCreatedAt() { @@ -177,12 +176,12 @@ public class AppointmentResponse { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AppointmentResponse that = (AppointmentResponse) o; - return Objects.equals(appointmentId, that.appointmentId) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petNames, that.petNames) && Objects.equals(petIds, that.petIds) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); + return Objects.equals(appointmentId, that.appointmentId) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(serviceId, that.serviceId) && Objects.equals(serviceName, that.serviceName) && Objects.equals(appointmentDate, that.appointmentDate) && Objects.equals(appointmentTime, that.appointmentTime) && Objects.equals(appointmentStatus, that.appointmentStatus) && Objects.equals(petName, that.petName) && Objects.equals(petId, that.petId) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt); } @Override public int hashCode() { - return Objects.hash(appointmentId, customerId, customerName, storeId, storeName, serviceId, serviceName, appointmentDate, appointmentTime, appointmentStatus, petNames, petIds, createdAt, updatedAt); + return Objects.hash(appointmentId, customerId, customerName, storeId, storeName, serviceId, serviceName, appointmentDate, appointmentTime, appointmentStatus, petName, petId, createdAt, updatedAt); } @Override @@ -198,8 +197,8 @@ public class AppointmentResponse { ", appointmentDate=" + appointmentDate + ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + - ", petNames=" + petNames + - ", petIds=" + petIds + + ", petName='" + petName + '\'' + + ", petId=" + petId + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; diff --git a/backend/src/main/java/com/petshop/backend/entity/Appointment.java b/backend/src/main/java/com/petshop/backend/entity/Appointment.java index f313d928..0e80f58e 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Appointment.java +++ b/backend/src/main/java/com/petshop/backend/entity/Appointment.java @@ -7,9 +7,7 @@ import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; @Entity @Table(name = "appointment") @@ -44,13 +42,9 @@ public class Appointment { @Column(nullable = false, length = 20) private String appointmentStatus; - @ManyToMany - @JoinTable( - name = "appointmentPet", - joinColumns = @JoinColumn(name = "appointmentId"), - inverseJoinColumns = @JoinColumn(name = "petId") - ) - private Set pets = new HashSet<>(); + @ManyToOne + @JoinColumn(name = "petId") + private Pet pet; @CreationTimestamp @Column(name = "created_at", updatable = false) @@ -127,12 +121,12 @@ public class Appointment { this.appointmentStatus = appointmentStatus; } - public Set getPets() { - return pets; + public Pet getPet() { + return pet; } - public void setPets(Set pets) { - this.pets = pets; + public void setPet(Pet pet) { + this.pet = pet; } public LocalDateTime getCreatedAt() { @@ -175,7 +169,7 @@ public class Appointment { ", appointmentDate=" + appointmentDate + ", appointmentTime=" + appointmentTime + ", appointmentStatus='" + appointmentStatus + '\'' + - ", pets=" + pets + + ", pet=" + pet + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 98aac596..5c79d7a6 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -20,7 +20,7 @@ spring: jpa: hibernate: - ddl-auto: none + ddl-auto: validate naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: ${JPA_SHOW_SQL:false} @@ -32,7 +32,7 @@ spring: flyway: enabled: true baseline-on-migrate: true - baseline-version: 0 + baseline-version: 1 server: port: ${SERVER_PORT:8080} diff --git a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java b/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java deleted file mode 100644 index e7caa86e..00000000 --- a/backend/src/test/java/com/petshop/backend/controller/DropdownControllerTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.petshop.backend.controller; - -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.EmployeeStore; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.StoreLocation; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.CategoryRepository; -import com.petshop.backend.repository.CustomerPetRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeStoreRepository; -import com.petshop.backend.repository.PetRepository; -import com.petshop.backend.repository.ProductRepository; -import com.petshop.backend.repository.ServiceRepository; -import com.petshop.backend.repository.StoreRepository; -import com.petshop.backend.repository.SupplierRepository; -import com.petshop.backend.repository.UserRepository; -import com.petshop.backend.security.AppPrincipal; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class DropdownControllerTest { - - private PetRepository petRepository; - private CustomerRepository customerRepository; - private CustomerPetRepository customerPetRepository; - private ServiceRepository serviceRepository; - private ProductRepository productRepository; - private CategoryRepository categoryRepository; - private StoreRepository storeRepository; - private SupplierRepository supplierRepository; - private EmployeeStoreRepository employeeStoreRepository; - private UserRepository userRepository; - private DropdownController controller; - - @BeforeEach - void setUp() { - petRepository = mock(PetRepository.class); - customerRepository = mock(CustomerRepository.class); - customerPetRepository = mock(CustomerPetRepository.class); - serviceRepository = mock(ServiceRepository.class); - productRepository = mock(ProductRepository.class); - categoryRepository = mock(CategoryRepository.class); - storeRepository = mock(StoreRepository.class); - supplierRepository = mock(SupplierRepository.class); - employeeStoreRepository = mock(EmployeeStoreRepository.class); - userRepository = mock(UserRepository.class); - - controller = new DropdownController( - petRepository, - customerRepository, - customerPetRepository, - serviceRepository, - productRepository, - categoryRepository, - storeRepository, - supplierRepository, - employeeStoreRepository, - userRepository - ); - } - - @AfterEach - void tearDown() { - SecurityContextHolder.clearContext(); - } - - private void setAuthentication(Long userId, User.Role role) { - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken( - new AppPrincipal(userId, "user", role, 0), - null, - List.of() - ) - ); - } - - @Test - void getStoreEmployeesReturnsOnlyStaffLinkedEmployees() { - StoreLocation store = new StoreLocation(); - store.setStoreId(1L); - - Employee staffEmployee = new Employee(); - staffEmployee.setEmployeeId(7L); - staffEmployee.setUserId(7L); - staffEmployee.setFirstName("Alex"); - staffEmployee.setLastName("Jones"); - staffEmployee.setIsActive(true); - - Employee adminEmployee = new Employee(); - adminEmployee.setEmployeeId(8L); - adminEmployee.setUserId(8L); - adminEmployee.setFirstName("Admin"); - adminEmployee.setLastName("Helper"); - adminEmployee.setIsActive(true); - - User staffUser = new User(); - staffUser.setId(7L); - staffUser.setRole(User.Role.STAFF); - staffUser.setActive(true); - - User adminUser = new User(); - adminUser.setId(8L); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(staffEmployee, store), new EmployeeStore(adminEmployee, store))); - when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); - when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); - - var response = controller.getStoreEmployees(1L); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - } - - @Test - void getStoreEmployeesReturnsAllStaffWhenStoreIdIsNull() { - StoreLocation store = new StoreLocation(); - store.setStoreId(1L); - - Employee staffEmployee = new Employee(); - staffEmployee.setEmployeeId(7L); - staffEmployee.setUserId(7L); - staffEmployee.setFirstName("Alex"); - staffEmployee.setLastName("Jones"); - staffEmployee.setIsActive(true); - - User staffUser = new User(); - staffUser.setId(7L); - staffUser.setRole(User.Role.STAFF); - staffUser.setActive(true); - - when(employeeStoreRepository.findActiveAllOrderByEmployeeEmployeeIdAsc()) - .thenReturn(List.of(new EmployeeStore(staffEmployee, store))); - when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); - - var response = controller.getStoreEmployees(null); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(7L), response.getBody().get(0).getId()); - } - - @Test - void getStoreEmployeesExcludesInactiveStaffUsers() { - StoreLocation store = new StoreLocation(); - store.setStoreId(1L); - - Employee inactiveStaffEmployee = new Employee(); - inactiveStaffEmployee.setEmployeeId(7L); - inactiveStaffEmployee.setUserId(7L); - inactiveStaffEmployee.setFirstName("Alex"); - inactiveStaffEmployee.setLastName("Jones"); - inactiveStaffEmployee.setIsActive(true); - - User inactiveStaffUser = new User(); - inactiveStaffUser.setId(7L); - inactiveStaffUser.setRole(User.Role.STAFF); - inactiveStaffUser.setActive(false); - - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(inactiveStaffEmployee, store))); - when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); - - var response = controller.getStoreEmployees(1L); - - assertEquals(0, response.getBody().size()); - } - - @Test - void getAppointmentCustomersReturnsOnlyCustomersWithPetsForStaff() { - User staffUser = new User(); - staffUser.setId(99L); - staffUser.setRole(User.Role.STAFF); - when(userRepository.findById(99L)).thenReturn(Optional.of(staffUser)); - setAuthentication(99L, User.Role.STAFF); - - Customer one = new Customer(); - one.setCustomerId(1L); - one.setFirstName("Alex"); - one.setLastName("Brown"); - - when(customerRepository.findAllWithPets()).thenReturn(List.of(one)); - - var response = controller.getAppointmentCustomers(); - - assertEquals(1, response.getBody().size()); - assertEquals(Long.valueOf(1L), response.getBody().get(0).getId()); - } -} diff --git a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java deleted file mode 100644 index a133c29f..00000000 --- a/backend/src/test/java/com/petshop/backend/service/AdoptionServiceTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.adoption.AdoptionRequest; -import com.petshop.backend.entity.Adoption; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.Pet; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.AdoptionRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; -import com.petshop.backend.repository.PetRepository; -import com.petshop.backend.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.quality.Strictness; - -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AdoptionServiceTest { - - @Mock private AdoptionRepository adoptionRepository; - @Mock private PetRepository petRepository; - @Mock private CustomerRepository customerRepository; - @Mock private EmployeeRepository employeeRepository; - @Mock private UserRepository userRepository; - - @InjectMocks - private AdoptionService adoptionService; - - private Pet pet; - private Customer customer; - private Employee staffEmployee; - private Employee adminEmployee; - - @BeforeEach - void setUp() { - pet = new Pet(); - pet.setPetId(1L); - pet.setPetName("Buddy"); - pet.setPetStatus("Available"); - - customer = new Customer(); - customer.setCustomerId(1L); - customer.setFirstName("Pat"); - customer.setLastName("Owner"); - - staffEmployee = new Employee(); - staffEmployee.setEmployeeId(7L); - staffEmployee.setUserId(7L); - staffEmployee.setFirstName("Alex"); - staffEmployee.setLastName("Jones"); - staffEmployee.setIsActive(true); - - adminEmployee = new Employee(); - adminEmployee.setEmployeeId(8L); - adminEmployee.setUserId(8L); - adminEmployee.setFirstName("Admin"); - adminEmployee.setLastName("Helper"); - adminEmployee.setIsActive(true); - - User staffUser = new User(); - staffUser.setId(7L); - staffUser.setRole(User.Role.STAFF); - staffUser.setActive(true); - when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); - - User adminUser = new User(); - adminUser.setId(8L); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - when(userRepository.findById(8L)).thenReturn(Optional.of(adminUser)); - } - - @Test - void createAdoptionAutoAssignsFirstStaffEmployee() { - when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - - when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee)); - when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> { - Adoption adoption = invocation.getArgument(0); - adoption.setAdoptionId(10L); - return adoption; - }); - - AdoptionRequest request = new AdoptionRequest(); - request.setPetId(1L); - request.setCustomerId(1L); - request.setAdoptionDate(LocalDate.now()); - request.setAdoptionStatus("Pending"); - - var response = adoptionService.createAdoption(request); - - assertEquals(7L, response.getEmployeeId()); - assertEquals("Alex Jones", response.getEmployeeName()); - } - - @Test - void createAdoptionRejectsAdminEmployeeSelection() { - when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - - AdoptionRequest request = new AdoptionRequest(); - request.setPetId(1L); - request.setCustomerId(1L); - request.setEmployeeId(8L); - request.setAdoptionDate(LocalDate.now()); - request.setAdoptionStatus("Pending"); - - assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); - } - - @Test - void createAdoptionRejectsInactiveStaffUserSelection() { - User inactiveStaffUser = new User(); - inactiveStaffUser.setId(7L); - inactiveStaffUser.setRole(User.Role.STAFF); - inactiveStaffUser.setActive(false); - when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); - - when(petRepository.findById(1L)).thenReturn(Optional.of(pet)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(employeeRepository.findById(7L)).thenReturn(Optional.of(staffEmployee)); - - AdoptionRequest request = new AdoptionRequest(); - request.setPetId(1L); - request.setCustomerId(1L); - request.setEmployeeId(7L); - request.setAdoptionDate(LocalDate.now()); - request.setAdoptionStatus("Pending"); - - assertThrows(IllegalArgumentException.class, () -> adoptionService.createAdoption(request)); - } -} diff --git a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java b/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java deleted file mode 100644 index 3e6f2d89..00000000 --- a/backend/src/test/java/com/petshop/backend/service/AppointmentServiceTest.java +++ /dev/null @@ -1,332 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.entity.Appointment; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.CustomerPet; -import com.petshop.backend.entity.Employee; -import com.petshop.backend.entity.EmployeeStore; -import com.petshop.backend.entity.Pet; -import com.petshop.backend.entity.Service; -import com.petshop.backend.entity.StoreLocation; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.AppointmentRepository; -import com.petshop.backend.repository.CustomerPetRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.EmployeeRepository; -import com.petshop.backend.repository.EmployeeStoreRepository; -import com.petshop.backend.repository.PetRepository; -import com.petshop.backend.repository.ServiceRepository; -import com.petshop.backend.repository.StoreRepository; -import com.petshop.backend.repository.UserRepository; -import com.petshop.backend.security.AppPrincipal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.quality.Strictness; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AppointmentServiceTest { - - @Mock private AppointmentRepository appointmentRepository; - @Mock private CustomerRepository customerRepository; - @Mock private CustomerPetRepository customerPetRepository; - @Mock private ServiceRepository serviceRepository; - @Mock private PetRepository petRepository; - @Mock private StoreRepository storeRepository; - @Mock private UserRepository userRepository; - @Mock private EmployeeRepository employeeRepository; - @Mock private EmployeeStoreRepository employeeStoreRepository; - - @InjectMocks - private AppointmentService appointmentService; - - private Customer customer; - private StoreLocation store; - private Service grooming; - private Service nailTrim; - private Pet pet; - private CustomerPet customerPet; - private Employee employee; - private LocalDate date; - - @BeforeEach - void setUp() { - setAuthentication(99L, User.Role.ADMIN); - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - - customer = new Customer(); - customer.setCustomerId(1L); - customer.setFirstName("Pat"); - customer.setLastName("Owner"); - - store = new StoreLocation(); - store.setStoreId(1L); - store.setStoreName("Main Store"); - - grooming = new Service(); - grooming.setServiceId(1L); - grooming.setServiceName("Grooming"); - grooming.setServiceDuration(30); - - nailTrim = new Service(); - nailTrim.setServiceId(2L); - nailTrim.setServiceName("Nail Trim"); - nailTrim.setServiceDuration(30); - - pet = new Pet(); - pet.setPetId(1L); - pet.setPetName("Milo"); - - customerPet = new CustomerPet(); - customerPet.setCustomerPetId(11L); - customerPet.setPetName("Milo Jr"); - customerPet.setCustomer(customer); - - employee = new Employee(); - employee.setEmployeeId(7L); - employee.setUserId(7L); - employee.setFirstName("Alex"); - employee.setLastName("Jones"); - employee.setIsActive(true); - - User staffUser = new User(); - staffUser.setId(7L); - staffUser.setRole(User.Role.STAFF); - staffUser.setActive(true); - when(userRepository.findById(7L)).thenReturn(Optional.of(staffUser)); - - date = LocalDate.now().plusDays(1); - } - - @AfterEach - void tearDown() { - SecurityContextHolder.clearContext(); - } - - @Test - void checkAvailabilityAllowsConcurrentAppointmentsIfAnotherEmployeeFree() { - Employee employee2 = new Employee(); - employee2.setEmployeeId(8L); - employee2.setUserId(8L); - employee2.setFirstName("Bob"); - employee2.setIsActive(true); - - User staffUser2 = new User(); - staffUser2.setId(8L); - staffUser2.setRole(User.Role.STAFF); - staffUser2.setActive(true); - when(userRepository.findById(8L)).thenReturn(Optional.of(staffUser2)); - - Appointment existing = appointment(1L, date, LocalTime.of(10, 0), grooming, store); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(2L)).thenReturn(Optional.of(nailTrim)); - - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store), new EmployeeStore(employee2, store))); - - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of(existing)); - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(8L, date)).thenReturn(List.of()); - - List slots = appointmentService.checkAvailability(1L, 2L, date); - - assertTrue(slots.contains("10:00")); - } - - @Test - void createAppointmentRejectsCustomerPetOwnedByDifferentCustomerForStaff() { - setAuthentication(7L, User.Role.STAFF); - when(employeeRepository.findByUserId(7L)).thenReturn(Optional.of(employee)); - when(employeeStoreRepository.findByEmployeeEmployeeId(7L)).thenReturn(Optional.of(new EmployeeStore(employee, store))); - - Customer otherCustomer = new Customer(); - otherCustomer.setCustomerId(2L); - - CustomerPet otherCustomerPet = new CustomerPet(); - otherCustomerPet.setCustomerPetId(22L); - otherCustomerPet.setCustomer(otherCustomer); - otherCustomerPet.setPetName("Not Yours"); - - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(22L)); - - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); - } - - @Test - void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() { - setAuthentication(99L, User.Role.ADMIN); - - Customer otherCustomer = new Customer(); - otherCustomer.setCustomerId(2L); - - CustomerPet otherCustomerPet = new CustomerPet(); - otherCustomerPet.setCustomerPetId(22L); - otherCustomerPet.setCustomer(otherCustomer); - otherCustomerPet.setPetName("Not Yours"); - - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet)); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(22L)); - - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); - } - - @Test - void createAppointmentRejectsAdminEmployeeSelection() { - setAuthentication(99L, User.Role.ADMIN); - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - - Employee adminEmployee = new Employee(); - adminEmployee.setEmployeeId(8L); - adminEmployee.setUserId(8L); - adminEmployee.setFirstName("Admin"); - adminEmployee.setLastName("Helper"); - adminEmployee.setIsActive(true); - - User adminLinkedUser = new User(); - adminLinkedUser.setId(8L); - adminLinkedUser.setRole(User.Role.ADMIN); - adminLinkedUser.setActive(true); - - when(userRepository.findById(8L)).thenReturn(Optional.of(adminLinkedUser)); - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(employeeRepository.findById(8L)).thenReturn(Optional.of(adminEmployee)); - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(adminEmployee, store), new EmployeeStore(employee, store))); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setEmployeeId(8L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(11L)); - - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); - } - - @Test - void createAppointmentRejectsInactiveStaffUserSelection() { - setAuthentication(99L, User.Role.ADMIN); - User adminUser = new User(); - adminUser.setId(99L); - adminUser.setRole(User.Role.ADMIN); - adminUser.setActive(true); - when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser)); - - User inactiveStaffUser = new User(); - inactiveStaffUser.setId(7L); - inactiveStaffUser.setRole(User.Role.STAFF); - inactiveStaffUser.setActive(false); - when(userRepository.findById(7L)).thenReturn(Optional.of(inactiveStaffUser)); - - when(customerRepository.findById(1L)).thenReturn(Optional.of(customer)); - when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming)); - when(employeeRepository.findById(7L)).thenReturn(Optional.of(employee)); - when(appointmentRepository.findByEmployeeEmployeeIdAndAppointmentDate(7L, date)).thenReturn(List.of()); - when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet)); - when(employeeStoreRepository.findActiveByStoreStoreIdOrderByEmployeeEmployeeIdAsc(1L)) - .thenReturn(List.of(new EmployeeStore(employee, store))); - - var request = new com.petshop.backend.dto.appointment.AppointmentRequest(); - request.setCustomerId(1L); - request.setStoreId(1L); - request.setServiceId(1L); - request.setEmployeeId(7L); - request.setAppointmentDate(date); - request.setAppointmentTime(LocalTime.of(10, 0)); - request.setAppointmentStatus("Booked"); - request.setCustomerPetIds(List.of(11L)); - - assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request)); - } - - private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) { - Appointment appointment = new Appointment(); - appointment.setAppointmentId(id); - appointment.setAppointmentDate(date); - appointment.setAppointmentTime(time); - appointment.setAppointmentStatus("Booked"); - appointment.setService(service); - appointment.setStore(storeLocation); - appointment.setEmployee(employee); - appointment.setCustomer(customer); - appointment.setPets(Set.of()); - appointment.setCustomerPets(Set.of()); - return appointment; - } - - private void setAuthentication(Long userId, User.Role role) { - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken( - new AppPrincipal(userId, "user", role, 0), - "n/a", - List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) - ) - ); - } -} diff --git a/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java b/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java deleted file mode 100644 index 4ce13bd2..00000000 --- a/backend/src/test/java/com/petshop/backend/service/ChatServiceTest.java +++ /dev/null @@ -1,199 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.dto.chat.MessageRequest; -import com.petshop.backend.dto.chat.UpdateConversationRequest; -import com.petshop.backend.entity.Conversation; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Message; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.ConversationRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.MessageRepository; -import com.petshop.backend.repository.UserRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.security.access.AccessDeniedException; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ChatServiceTest { - - @Mock - private ConversationRepository conversationRepository; - - @Mock - private MessageRepository messageRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private CustomerRepository customerRepository; - - @InjectMocks - private ChatService chatService; - - private Customer customer; - - @BeforeEach - void setUp() { - customer = new Customer(); - customer.setCustomerId(1L); - customer.setUserId(10L); - customer.setFirstName("Pat"); - customer.setLastName("Owner"); - customer.setEmail("pat@example.com"); - } - - @Test - void updateConversationMarksConversationClosed() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)) - .thenReturn(List.of(message("hello"))); - - var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")); - - assertEquals("CLOSED", response.getStatus()); - assertEquals("hello", response.getLastMessage()); - verify(conversationRepository).save(conversation); - } - - @Test - void updateConversationRejectsOtherCustomer() { - Conversation conversation = conversation(99L, 2L, null, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - - assertThrows(AccessDeniedException.class, - () -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED"))); - } - - @Test - void updateConversationIsIdempotent() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); - - var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("CLOSED")); - - assertEquals("CLOSED", response.getStatus()); - } - - @Test - void staffCanCloseAssignedConversation() { - Conversation conversation = conversation(99L, 1L, 77L, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); - - var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED")); - - assertEquals("CLOSED", response.getStatus()); - } - - @Test - void staffCanCloseUnassignedConversation() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); - - var response = chatService.updateConversation(99L, 77L, User.Role.STAFF, new UpdateConversationRequest("CLOSED")); - - assertEquals("CLOSED", response.getStatus()); - } - - @Test - void adminCanCloseAnyConversation() { - Conversation conversation = conversation(99L, 2L, 88L, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); - - var response = chatService.updateConversation(99L, 1L, User.Role.ADMIN, new UpdateConversationRequest("CLOSED")); - - assertEquals("CLOSED", response.getStatus()); - } - - @Test - void updateConversationCanReopenClosedConversation() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - when(conversationRepository.save(any(Conversation.class))).thenAnswer(invocation -> invocation.getArgument(0)); - when(messageRepository.findByConversationIdOrderByTimestampAsc(99L)).thenReturn(List.of()); - - var response = chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("OPEN")); - - assertEquals("OPEN", response.getStatus()); - } - - @Test - void updateConversationRejectsInvalidStatus() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.OPEN); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - when(customerRepository.findByUserId(10L)).thenReturn(Optional.of(customer)); - - assertThrows(IllegalArgumentException.class, - () -> chatService.updateConversation(99L, 10L, User.Role.CUSTOMER, new UpdateConversationRequest("INVALID"))); - } - - @Test - void sendMessageRejectsClosedConversation() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - - assertThrows(AccessDeniedException.class, - () -> chatService.sendMessage(99L, 10L, User.Role.CUSTOMER, new MessageRequest("hello"))); - - verify(messageRepository, never()).save(any()); - } - - @Test - void requestHumanTakeoverRejectsClosedConversation() { - Conversation conversation = conversation(99L, 1L, null, Conversation.ConversationStatus.CLOSED); - when(conversationRepository.findById(99L)).thenReturn(Optional.of(conversation)); - - assertThrows(AccessDeniedException.class, - () -> chatService.requestHumanTakeover(99L, 10L, User.Role.CUSTOMER)); - } - - private Conversation conversation(Long id, Long customerId, Long staffId, Conversation.ConversationStatus status) { - Conversation conversation = new Conversation(); - conversation.setId(id); - conversation.setCustomerId(customerId); - conversation.setStaffId(staffId); - conversation.setStatus(status); - conversation.setMode(Conversation.ConversationMode.AUTOMATED); - conversation.setHumanRequestedAt(LocalDateTime.now()); - return conversation; - } - - private Message message(String content) { - Message message = new Message(); - message.setConversationId(99L); - message.setSenderId(10L); - message.setContent(content); - message.setIsRead(false); - return message; - } -} diff --git a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java deleted file mode 100644 index 6a80e9ca..00000000 --- a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.petshop.backend.service; - -import com.petshop.backend.entity.Adoption; -import com.petshop.backend.entity.Customer; -import com.petshop.backend.entity.Pet; -import com.petshop.backend.entity.User; -import com.petshop.backend.exception.ResourceNotFoundException; -import com.petshop.backend.repository.AdoptionRepository; -import com.petshop.backend.repository.CustomerRepository; -import com.petshop.backend.repository.PetRepository; -import com.petshop.backend.repository.StoreRepository; -import com.petshop.backend.security.AppPrincipal; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class PetServiceTest { - - @Mock - private PetRepository petRepository; - - @Mock - private AdoptionRepository adoptionRepository; - - @Mock - private CustomerRepository customerRepository; - - @Mock - private StoreRepository storeRepository; - - @Mock - private CatalogImageStorageService catalogImageStorageService; - - @InjectMocks - private PetService petService; - - @AfterEach - void tearDown() { - SecurityContextHolder.clearContext(); - } - - @Test - void getAllPetsAnonymousReturnsOnlyPublicPets() { - Pageable pageable = PageRequest.of(0, 10); - Pet availablePet = pet(1L, "Buddy", "Available"); - when(petRepository.searchPublicPets(null, null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); - - var result = petService.getAllPets(null, null, null, null, pageable); - - assertEquals(1, result.getTotalElements()); - assertEquals("Buddy", result.getContent().get(0).getPetName()); - verify(petRepository).searchPublicPets(null, null, null, pageable); - verify(petRepository, never()).searchPets(null, null, null, null, pageable); - } - - @Test - void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { - Pageable pageable = PageRequest.of(0, 10); - - var result = petService.getAllPets(null, null, "Adopted", null, pageable); - - assertEquals(0, result.getTotalElements()); - verify(petRepository, never()).searchPublicPets(null, null, null, pageable); - } - - @Test - void getAllPetsCustomerReturnsVisiblePetsOnly() { - Pageable pageable = PageRequest.of(0, 10); - setAuthentication(25L, User.Role.CUSTOMER); - Pet availablePet = pet(1L, "Buddy", "Available"); - Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) - .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - - var result = petService.getAllPets(null, null, null, null, pageable); - - assertEquals(2, result.getTotalElements()); - verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); - } - - @Test - void getAllPetsAdminReturnsAllPets() { - Pageable pageable = PageRequest.of(0, 10); - setAuthentication(99L, User.Role.ADMIN); - Pet availablePet = pet(1L, "Buddy", "Available"); - Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.searchPets(null, null, null, null, pageable)) - .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - - var result = petService.getAllPets(null, null, null, null, pageable); - - assertEquals(2, result.getTotalElements()); - verify(petRepository).searchPets(null, null, null, null, pageable); - } - - @Test - void getPetByIdHidesAdoptedPetFromUnrelatedCustomer() { - setAuthentication(50L, User.Role.CUSTOMER); - Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); - when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) - .thenReturn(Optional.of(adoption(2L, 25L))); - - assertThrows(ResourceNotFoundException.class, () -> petService.getPetById(2L)); - } - - @Test - void getPetByIdAllowsOwnerToSeeAdoptedPet() { - setAuthentication(25L, User.Role.CUSTOMER); - Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.findById(2L)).thenReturn(Optional.of(adoptedPet)); - when(adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(2L, "Completed")) - .thenReturn(Optional.of(adoption(2L, 25L))); - - var result = petService.getPetById(2L); - - assertEquals(2L, result.getPetId()); - } - - private void setAuthentication(Long userId, User.Role role) { - SecurityContextHolder.getContext().setAuthentication( - new UsernamePasswordAuthenticationToken( - new AppPrincipal(userId, "user", role, 0), - "n/a", - List.of(new SimpleGrantedAuthority("ROLE_" + role.name())) - ) - ); - } - - private Pet pet(Long id, String name, String status) { - Pet pet = new Pet(); - pet.setPetId(id); - pet.setPetName(name); - pet.setPetSpecies("Cat"); - pet.setPetBreed("Mixed"); - pet.setPetAge(2); - pet.setPetStatus(status); - pet.setPetPrice(java.math.BigDecimal.TEN); - return pet; - } - - private Adoption adoption(Long petId, Long userId) { - Adoption adoption = new Adoption(); - Pet pet = new Pet(); - pet.setPetId(petId); - adoption.setPet(pet); - Customer customer = new Customer(); - customer.setCustomerId(1L); - customer.setUserId(userId); - adoption.setCustomer(customer); - adoption.setAdoptionStatus("Completed"); - return adoption; - } -} From 2795511a41c821e816469acb2f3048ac15deb62f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 20:56:14 -0600 Subject: [PATCH 094/137] fix lazy loading on me, services, refunds --- .../java/com/petshop/backend/controller/AuthController.java | 2 ++ backend/src/main/java/com/petshop/backend/entity/Service.java | 2 +- .../main/java/com/petshop/backend/service/RefundService.java | 2 ++ .../main/java/com/petshop/backend/service/ServiceService.java | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 641c7a6b..c237f27e 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -25,6 +25,7 @@ import org.springframework.security.authentication.InternalAuthenticationService import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -130,6 +131,7 @@ public class AuthController { } } + @Transactional(readOnly = true) @GetMapping("/me") public ResponseEntity getCurrentUser() { User user = getAuthenticatedUser(); diff --git a/backend/src/main/java/com/petshop/backend/entity/Service.java b/backend/src/main/java/com/petshop/backend/entity/Service.java index 77223922..6b6c4267 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Service.java +++ b/backend/src/main/java/com/petshop/backend/entity/Service.java @@ -30,7 +30,7 @@ public class Service { @Column(nullable = false) private Integer serviceDuration; - @ElementCollection + @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "service_species", joinColumns = @JoinColumn(name = "serviceId")) @Column(name = "species", length = 50) private Set species = new HashSet<>(); diff --git a/backend/src/main/java/com/petshop/backend/service/RefundService.java b/backend/src/main/java/com/petshop/backend/service/RefundService.java index 6d3ef880..49cc2f69 100644 --- a/backend/src/main/java/com/petshop/backend/service/RefundService.java +++ b/backend/src/main/java/com/petshop/backend/service/RefundService.java @@ -81,6 +81,7 @@ public class RefundService { return toResponse(savedRefund); } + @Transactional(readOnly = true) public RefundResponse getRefundById(Long id, Long customerId) { Refund refund = refundRepository.findById(id) .orElseThrow(() -> new RuntimeException("Refund not found")); @@ -92,6 +93,7 @@ public class RefundService { return toResponse(refund); } + @Transactional(readOnly = true) public List getAllRefunds(Long customerId) { List refunds; diff --git a/backend/src/main/java/com/petshop/backend/service/ServiceService.java b/backend/src/main/java/com/petshop/backend/service/ServiceService.java index e2395392..65166f44 100644 --- a/backend/src/main/java/com/petshop/backend/service/ServiceService.java +++ b/backend/src/main/java/com/petshop/backend/service/ServiceService.java @@ -19,6 +19,7 @@ public class ServiceService { this.serviceRepository = serviceRepository; } + @Transactional(readOnly = true) public Page getAllServices(String query, Pageable pageable) { Page services; if (query != null && !query.trim().isEmpty()) { @@ -29,6 +30,7 @@ public class ServiceService { return services.map(this::mapToResponse); } + @Transactional(readOnly = true) public ServiceResponse getServiceById(Long id) { com.petshop.backend.entity.Service service = serviceRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + id)); From e631ae2953007401e593409f47ea170d77c25996 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 21:01:20 -0600 Subject: [PATCH 095/137] scope inventory lookup by store on sale --- .../petshop/backend/repository/InventoryRepository.java | 3 +++ .../java/com/petshop/backend/service/SaleService.java | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java index 69ff16ed..b448b497 100644 --- a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java @@ -16,6 +16,9 @@ public interface InventoryRepository extends JpaRepository { @Query("SELECT i FROM Inventory i WHERE i.product.prodId = :productId") Optional findByProductId(@Param("productId") Long productId); + @Query("SELECT i FROM Inventory i WHERE i.product.prodId = :productId AND i.store.storeId = :storeId") + Optional findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId); + @Query("SELECT i FROM Inventory i LEFT JOIN i.store s WHERE " + "LOWER(i.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index 643cfc2a..c90ad659 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -134,8 +134,8 @@ public class SaleService { " for product: " + product.getProdName()); } - Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) - .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + Inventory inventory = inventoryRepository.findByProductIdAndStoreId(itemRequest.getProdId(), store.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId() + " at store " + store.getStoreId())); inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity()); inventoryRepository.save(inventory); @@ -158,8 +158,8 @@ public class SaleService { Product product = productRepository.findById(itemRequest.getProdId()) .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); - Inventory inventory = inventoryRepository.findByProductId(itemRequest.getProdId()) - .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId())); + Inventory inventory = inventoryRepository.findByProductIdAndStoreId(itemRequest.getProdId(), store.getStoreId()) + .orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProdId() + " at store " + store.getStoreId())); if (inventory.getQuantity() < itemRequest.getQuantity()) { throw new BusinessException("Insufficient stock for product: " + product.getProdName() + From 1187c7bcc1e613952e465f91fdf19fb4c0c86ae4 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 21:06:01 -0600 Subject: [PATCH 096/137] point to port 3306 Petstoredb --- backend/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5c79d7a6..07b2c86b 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: max-request-size: 5MB datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3307/Petstoredb_target?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} + url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} username: ${SPRING_DATASOURCE_USERNAME:petshop} password: ${SPRING_DATASOURCE_PASSWORD:petshop} driver-class-name: com.mysql.cj.jdbc.Driver From cf338920dd88d0f3ac0884a24343b5cb7f7ad9f3 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 21:13:17 -0600 Subject: [PATCH 097/137] fix local seed: add missing categories and storeId in inventory insert --- .../resources/dev/expand_pet_product_seed.sql | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/src/main/resources/dev/expand_pet_product_seed.sql b/backend/src/main/resources/dev/expand_pet_product_seed.sql index 63736dda..296bc7f6 100644 --- a/backend/src/main/resources/dev/expand_pet_product_seed.sql +++ b/backend/src/main/resources/dev/expand_pet_product_seed.sql @@ -97,6 +97,13 @@ VALUES ('Sprout', 'Hamster', 'Roborovski', 1, 'Available', 26.00), ('Bean', 'Hamster', 'Syrian', 2, 'Available', 28.00); +INSERT IGNORE INTO category (categoryId, categoryName, categoryType) VALUES +(1, 'Dog Food', 'Product'), +(2, 'Cat Toys', 'Product'), +(3, 'Bird Supplies', 'Product'), +(4, 'Aquarium', 'Product'), +(5, 'Small Animals', 'Product'); + INSERT INTO product (prodName, prodPrice, categoryId, prodDesc) VALUES ('Chicken Recipe Dog Food', 42.00, 1, 'Nutritious food and treats for dogs'), @@ -210,8 +217,9 @@ WHERE p.prodId >= 7 SELECT 1 FROM productSupplier ps WHERE ps.prodId = p.prodId ); -INSERT INTO inventory (prodId, quantity) -SELECT p.prodId, +INSERT INTO inventory (storeId, prodId, quantity) +SELECT s.storeId, + p.prodId, CASE p.categoryId WHEN 1 THEN 120 + MOD((p.prodId - 7) * 17, 60) WHEN 2 THEN 180 + MOD((p.prodId - 7) * 17, 60) @@ -220,7 +228,8 @@ SELECT p.prodId, ELSE 95 + MOD((p.prodId - 7) * 17, 60) END FROM product p +CROSS JOIN storeLocation s WHERE p.prodId >= 7 AND NOT EXISTS ( - SELECT 1 FROM inventory i WHERE i.prodId = p.prodId + SELECT 1 FROM inventory i WHERE i.prodId = p.prodId AND i.storeId = s.storeId ); From e3ebb93dd246e9ba000036f2767589e5ca86bbc7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 21:18:30 -0600 Subject: [PATCH 098/137] seed stores and suppliers before products on fresh DB --- .../main/resources/dev/expand_pet_product_seed.sql | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/main/resources/dev/expand_pet_product_seed.sql b/backend/src/main/resources/dev/expand_pet_product_seed.sql index 296bc7f6..b51c3dec 100644 --- a/backend/src/main/resources/dev/expand_pet_product_seed.sql +++ b/backend/src/main/resources/dev/expand_pet_product_seed.sql @@ -97,6 +97,18 @@ VALUES ('Sprout', 'Hamster', 'Roborovski', 1, 'Available', 26.00), ('Bean', 'Hamster', 'Syrian', 2, 'Available', 28.00); +INSERT IGNORE INTO storeLocation (storeId, storeName, address, phone, email) VALUES +(1, 'Downtown Branch', '123 Main St, Calgary, AB', '403-555-0101', 'downtown@petshop.com'), +(2, 'North Branch', '456 North Ave, Calgary, AB', '403-555-0102', 'north@petshop.com'), +(3, 'West Side Store', '789 West Blvd, Calgary, AB', '403-555-0103', 'westside@petshop.com'); + +INSERT IGNORE INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES +(1, 'PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '403-601-1001'), +(2, 'Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '403-601-1002'), +(3, 'Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '403-601-1003'), +(4, 'Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '403-601-1004'), +(5, 'Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '403-601-1005'); + INSERT IGNORE INTO category (categoryId, categoryName, categoryType) VALUES (1, 'Dog Food', 'Product'), (2, 'Cat Toys', 'Product'), From c86fbedd6fc3a68bcc52ba92eca8b7c7336fb58a Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:27:23 -0600 Subject: [PATCH 099/137] Fixed seeding for backend --- .../resources/db/migration/V2__seed_data.sql | 1863 +++++++++++++++++ 1 file changed, 1863 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V2__seed_data.sql diff --git a/backend/src/main/resources/db/migration/V2__seed_data.sql b/backend/src/main/resources/db/migration/V2__seed_data.sql new file mode 100644 index 00000000..aab02cc1 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__seed_data.sql @@ -0,0 +1,1863 @@ +-- Clear existing data before seeding +SET FOREIGN_KEY_CHECKS = 0; + +DELETE FROM activityLog; +DELETE FROM message; +DELETE FROM conversation; +DELETE FROM refund_item; +DELETE FROM refund; +DELETE FROM saleItem; +DELETE FROM sale; +DELETE FROM cart_item; +DELETE FROM cart; +DELETE FROM adoption; +DELETE FROM appointment; +DELETE FROM pet; +DELETE FROM coupon; +DELETE FROM purchaseOrder; +DELETE FROM inventory; +DELETE FROM productSupplier; +DELETE FROM product; +DELETE FROM service_species; +DELETE FROM service; +DELETE FROM category; +DELETE FROM supplier; +DELETE FROM users; +DELETE FROM storeLocation; + +ALTER TABLE storeLocation AUTO_INCREMENT = 1; +ALTER TABLE users AUTO_INCREMENT = 1; +ALTER TABLE supplier AUTO_INCREMENT = 1; +ALTER TABLE category AUTO_INCREMENT = 1; +ALTER TABLE service AUTO_INCREMENT = 1; +ALTER TABLE product AUTO_INCREMENT = 1; +ALTER TABLE inventory AUTO_INCREMENT = 1; +ALTER TABLE purchaseOrder AUTO_INCREMENT = 1; +ALTER TABLE coupon AUTO_INCREMENT = 1; +ALTER TABLE pet AUTO_INCREMENT = 1; +ALTER TABLE appointment AUTO_INCREMENT = 1; +ALTER TABLE adoption AUTO_INCREMENT = 1; +ALTER TABLE cart AUTO_INCREMENT = 1; +ALTER TABLE cart_item AUTO_INCREMENT = 1; +ALTER TABLE sale AUTO_INCREMENT = 1; +ALTER TABLE saleItem AUTO_INCREMENT = 1; +ALTER TABLE refund AUTO_INCREMENT = 1; +ALTER TABLE refund_item AUTO_INCREMENT = 1; +ALTER TABLE conversation AUTO_INCREMENT = 1; +ALTER TABLE message AUTO_INCREMENT = 1; +ALTER TABLE activityLog AUTO_INCREMENT = 1; + +SET FOREIGN_KEY_CHECKS = 1; + +-- Insert seed data +INSERT INTO storeLocation (storeId, storeName, address, phone, email, imageUrl) VALUES +(1, 'Downtown Branch', '123 Main St, Calgary, AB', '403-555-0101', 'downtown@petshop.com', 'https://images.petshop.local/stores/downtown.webp'), +(2, 'North Branch', '456 North Ave, Calgary, AB', '403-555-0102', 'north@petshop.com', 'https://images.petshop.local/stores/north.webp'), +(3, 'West Side Store', '789 West Blvd, Calgary, AB', '403-555-0103', 'westside@petshop.com', 'https://images.petshop.local/stores/west.webp'); + +INSERT INTO users (id, username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion) VALUES +(1, 'admin', '$2y$10$ok/BmOn/pyyamTeNmUDiB.OfLCduQlZSAaRLlupM/cZb7ZhiBriVe', 'admin@petshop.com', 'Admin', 'User', 'Admin User', '000-000-1000', 'https://images.petshop.local/users/001.webp', 'ADMIN', 'ADMINISTRATOR', 1, 0, 1, 0), +(2, 'morgan.lee', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'morgan.lee@petshop.com', 'Morgan', 'Lee', 'Morgan Lee', '403-700-0002', 'https://images.petshop.local/users/002.webp', 'ADMIN', 'OPERATIONS_ADMIN', 2, 0, 1, 0), +(3, 'staff', '$2y$10$23mqbLolo609T/.PC4KfiuY.9HqYEgA8LrJ/fccZ7CmK0/OIsPrfq', 'staff@petshop.com', 'Staff', 'User', 'Staff User', '000-000-1001', 'https://images.petshop.local/users/003.webp', 'STAFF', 'STORE_MANAGER', 1, 0, 1, 0), +(4, 'sara.smith', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sara.smith@petshop.com', 'Sara', 'Smith', 'Sara Smith', '403-710-0004', 'https://images.petshop.local/users/004.webp', 'STAFF', 'SALES_ASSOCIATE', 1, 0, 1, 0), +(5, 'david.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'david.brown@petshop.com', 'David', 'Brown', 'David Brown', '403-710-0005', 'https://images.petshop.local/users/005.webp', 'STAFF', 'VETERINARY_TECH', 1, 0, 1, 0), +(6, 'priya.patel', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'priya.patel@petshop.com', 'Priya', 'Patel', 'Priya Patel', '403-710-0006', 'https://images.petshop.local/users/006.webp', 'STAFF', 'GROOMER', 1, 0, 1, 0), +(7, 'michael.johnson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'michael.johnson@petshop.com', 'Michael', 'Johnson', 'Michael Johnson', '403-710-0007', 'https://images.petshop.local/users/007.webp', 'STAFF', 'STORE_MANAGER', 2, 0, 1, 0), +(8, 'emma.davis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'emma.davis@petshop.com', 'Emma', 'Davis', 'Emma Davis', '403-710-0008', 'https://images.petshop.local/users/008.webp', 'STAFF', 'SALES_ASSOCIATE', 2, 0, 1, 0), +(9, 'lucas.turner', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lucas.turner@petshop.com', 'Lucas', 'Turner', 'Lucas Turner', '403-710-0009', 'https://images.petshop.local/users/009.webp', 'STAFF', 'VETERINARY_TECH', 2, 0, 1, 0), +(10, 'nina.green', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nina.green@petshop.com', 'Nina', 'Green', 'Nina Green', '403-710-0010', 'https://images.petshop.local/users/010.webp', 'STAFF', 'GROOMER', 2, 0, 1, 0), +(11, 'lisa.williams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lisa.williams@petshop.com', 'Lisa', 'Williams', 'Lisa Williams', '403-710-0011', 'https://images.petshop.local/users/011.webp', 'STAFF', 'STORE_MANAGER', 3, 0, 1, 0), +(12, 'daniel.moore', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'daniel.moore@petshop.com', 'Daniel', 'Moore', 'Daniel Moore', '403-710-0012', 'https://images.petshop.local/users/012.webp', 'STAFF', 'SALES_ASSOCIATE', 3, 0, 1, 0), +(13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'VETERINARY_TECH', 3, 0, 1, 0), +(14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'GROOMER', 3, 0, 1, 0), +(15, 'customer', '$2y$10$fgIlTHDYUOzvbczwdhQP7..YuAHr2cGODb9OBQJqole3AkiY4CGUq', 'customer@petshop.com', 'Test', 'Customer', 'Test Customer', '000-000-1002', 'https://images.petshop.local/users/015.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0), +(16, 'alex.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brown@gmail.com', 'Alex', 'Brown', 'Alex Brown', '403-730-0016', 'https://images.petshop.local/users/016.webp', 'CUSTOMER', 'CUSTOMER', NULL, 12, 1, 0), +(17, 'alex.clark', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.clark@gmail.com', 'Alex', 'Clark', 'Alex Clark', '403-730-0017', 'https://images.petshop.local/users/017.webp', 'CUSTOMER', 'CUSTOMER', NULL, 15, 1, 0), +(18, 'alex.wilson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wilson@gmail.com', 'Alex', 'Wilson', 'Alex Wilson', '403-730-0018', 'https://images.petshop.local/users/018.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(19, 'alex.martinez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.martinez@gmail.com', 'Alex', 'Martinez', 'Alex Martinez', '403-730-0019', 'https://images.petshop.local/users/019.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(20, 'alex.anderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.anderson@gmail.com', 'Alex', 'Anderson', 'Alex Anderson', '403-730-0020', 'https://images.petshop.local/users/020.webp', 'CUSTOMER', 'CUSTOMER', NULL, 12, 1, 0), +(21, 'alex.taylor', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.taylor@gmail.com', 'Alex', 'Taylor', 'Alex Taylor', '403-730-0021', 'https://images.petshop.local/users/021.webp', 'CUSTOMER', 'CUSTOMER', NULL, 11, 1, 0), +(22, 'alex.parker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.parker@gmail.com', 'Alex', 'Parker', 'Alex Parker', '403-730-0022', 'https://images.petshop.local/users/022.webp', 'CUSTOMER', 'CUSTOMER', NULL, 16, 1, 0), +(23, 'alex.evans', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.evans@gmail.com', 'Alex', 'Evans', 'Alex Evans', '403-730-0023', 'https://images.petshop.local/users/023.webp', 'CUSTOMER', 'CUSTOMER', NULL, 36, 1, 0), +(24, 'alex.scott', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.scott@gmail.com', 'Alex', 'Scott', 'Alex Scott', '403-730-0024', 'https://images.petshop.local/users/024.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(25, 'alex.adams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.adams@gmail.com', 'Alex', 'Adams', 'Alex Adams', '403-730-0025', 'https://images.petshop.local/users/025.webp', 'CUSTOMER', 'CUSTOMER', NULL, 8, 1, 0), +(26, 'alex.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.baker@gmail.com', 'Alex', 'Baker', 'Alex Baker', '403-730-0026', 'https://images.petshop.local/users/026.webp', 'CUSTOMER', 'CUSTOMER', NULL, 29, 1, 0), +(27, 'alex.hall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hall@gmail.com', 'Alex', 'Hall', 'Alex Hall', '403-730-0027', 'https://images.petshop.local/users/027.webp', 'CUSTOMER', 'CUSTOMER', NULL, 3, 1, 0), +(28, 'alex.rivera', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.rivera@gmail.com', 'Alex', 'Rivera', 'Alex Rivera', '403-730-0028', 'https://images.petshop.local/users/028.webp', 'CUSTOMER', 'CUSTOMER', NULL, 13, 1, 0), +(29, 'alex.mitchell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mitchell@gmail.com', 'Alex', 'Mitchell', 'Alex Mitchell', '403-730-0029', 'https://images.petshop.local/users/029.webp', 'CUSTOMER', 'CUSTOMER', NULL, 30, 1, 0), +(30, 'alex.collins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.collins@gmail.com', 'Alex', 'Collins', 'Alex Collins', '403-730-0030', 'https://images.petshop.local/users/030.webp', 'CUSTOMER', 'CUSTOMER', NULL, 16, 1, 0), +(31, 'alex.morris', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.morris@gmail.com', 'Alex', 'Morris', 'Alex Morris', '403-730-0031', 'https://images.petshop.local/users/031.webp', 'CUSTOMER', 'CUSTOMER', NULL, 9, 1, 0), +(32, 'alex.cook', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cook@gmail.com', 'Alex', 'Cook', 'Alex Cook', '403-730-0032', 'https://images.petshop.local/users/032.webp', 'CUSTOMER', 'CUSTOMER', NULL, 19, 1, 0), +(33, 'alex.bell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bell@gmail.com', 'Alex', 'Bell', 'Alex Bell', '403-730-0033', 'https://images.petshop.local/users/033.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(34, 'alex.reed', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reed@gmail.com', 'Alex', 'Reed', 'Alex Reed', '403-730-0034', 'https://images.petshop.local/users/034.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(35, 'alex.murphy', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murphy@gmail.com', 'Alex', 'Murphy', 'Alex Murphy', '403-730-0035', 'https://images.petshop.local/users/035.webp', 'CUSTOMER', 'CUSTOMER', NULL, 31, 1, 0), +(36, 'alex.bailey', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bailey@gmail.com', 'Alex', 'Bailey', 'Alex Bailey', '403-730-0036', 'https://images.petshop.local/users/036.webp', 'CUSTOMER', 'CUSTOMER', NULL, 6, 1, 0), +(37, 'alex.cooper', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cooper@gmail.com', 'Alex', 'Cooper', 'Alex Cooper', '403-730-0037', 'https://images.petshop.local/users/037.webp', 'CUSTOMER', 'CUSTOMER', NULL, 4, 1, 0), +(38, 'alex.richardson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.richardson@gmail.com', 'Alex', 'Richardson', 'Alex Richardson', '403-730-0038', 'https://images.petshop.local/users/038.webp', 'CUSTOMER', 'CUSTOMER', NULL, 19, 1, 0), +(39, 'alex.cox', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cox@gmail.com', 'Alex', 'Cox', 'Alex Cox', '403-730-0039', 'https://images.petshop.local/users/039.webp', 'CUSTOMER', 'CUSTOMER', NULL, 4, 1, 0), +(40, 'alex.howard', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.howard@gmail.com', 'Alex', 'Howard', 'Alex Howard', '403-730-0040', 'https://images.petshop.local/users/040.webp', 'CUSTOMER', 'CUSTOMER', NULL, 12, 1, 0), +(41, 'alex.ward', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ward@gmail.com', 'Alex', 'Ward', 'Alex Ward', '403-730-0041', 'https://images.petshop.local/users/041.webp', 'CUSTOMER', 'CUSTOMER', NULL, 18, 1, 0), +(42, 'alex.torres', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.torres@gmail.com', 'Alex', 'Torres', 'Alex Torres', '403-730-0042', 'https://images.petshop.local/users/042.webp', 'CUSTOMER', 'CUSTOMER', NULL, 10, 1, 0), +(43, 'alex.peterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.peterson@gmail.com', 'Alex', 'Peterson', 'Alex Peterson', '403-730-0043', 'https://images.petshop.local/users/043.webp', 'CUSTOMER', 'CUSTOMER', NULL, 6, 1, 0), +(44, 'alex.gray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gray@gmail.com', 'Alex', 'Gray', 'Alex Gray', '403-730-0044', 'https://images.petshop.local/users/044.webp', 'CUSTOMER', 'CUSTOMER', NULL, 11, 1, 0), +(45, 'alex.ramirez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ramirez@gmail.com', 'Alex', 'Ramirez', 'Alex Ramirez', '403-730-0045', 'https://images.petshop.local/users/045.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(46, 'alex.james', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.james@gmail.com', 'Alex', 'James', 'Alex James', '403-730-0046', 'https://images.petshop.local/users/046.webp', 'CUSTOMER', 'CUSTOMER', NULL, 28, 1, 0), +(47, 'alex.watson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.watson@gmail.com', 'Alex', 'Watson', 'Alex Watson', '403-730-0047', 'https://images.petshop.local/users/047.webp', 'CUSTOMER', 'CUSTOMER', NULL, 8, 1, 0), +(48, 'alex.brooks', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brooks@gmail.com', 'Alex', 'Brooks', 'Alex Brooks', '403-730-0048', 'https://images.petshop.local/users/048.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(49, 'alex.kelly', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.kelly@gmail.com', 'Alex', 'Kelly', 'Alex Kelly', '403-730-0049', 'https://images.petshop.local/users/049.webp', 'CUSTOMER', 'CUSTOMER', NULL, 16, 1, 0), +(50, 'alex.sanders', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sanders@gmail.com', 'Alex', 'Sanders', 'Alex Sanders', '403-730-0050', 'https://images.petshop.local/users/050.webp', 'CUSTOMER', 'CUSTOMER', NULL, 21, 1, 0), +(51, 'alex.price', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.price@gmail.com', 'Alex', 'Price', 'Alex Price', '403-730-0051', 'https://images.petshop.local/users/051.webp', 'CUSTOMER', 'CUSTOMER', NULL, 7, 1, 0), +(52, 'alex.bennett', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bennett@gmail.com', 'Alex', 'Bennett', 'Alex Bennett', '403-730-0052', 'https://images.petshop.local/users/052.webp', 'CUSTOMER', 'CUSTOMER', NULL, 17, 1, 0), +(53, 'alex.wood', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wood@gmail.com', 'Alex', 'Wood', 'Alex Wood', '403-730-0053', 'https://images.petshop.local/users/053.webp', 'CUSTOMER', 'CUSTOMER', NULL, 10, 1, 0), +(54, 'alex.barnes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.barnes@gmail.com', 'Alex', 'Barnes', 'Alex Barnes', '403-730-0054', 'https://images.petshop.local/users/054.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(55, 'alex.ross', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ross@gmail.com', 'Alex', 'Ross', 'Alex Ross', '403-730-0055', 'https://images.petshop.local/users/055.webp', 'CUSTOMER', 'CUSTOMER', NULL, 7, 1, 0), +(56, 'alex.henderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.henderson@gmail.com', 'Alex', 'Henderson', 'Alex Henderson', '403-730-0056', 'https://images.petshop.local/users/056.webp', 'CUSTOMER', 'CUSTOMER', NULL, 15, 1, 0), +(57, 'alex.coleman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.coleman@gmail.com', 'Alex', 'Coleman', 'Alex Coleman', '403-730-0057', 'https://images.petshop.local/users/057.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(58, 'alex.jenkins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jenkins@gmail.com', 'Alex', 'Jenkins', 'Alex Jenkins', '403-730-0058', 'https://images.petshop.local/users/058.webp', 'CUSTOMER', 'CUSTOMER', NULL, 17, 1, 0), +(59, 'alex.perry', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.perry@gmail.com', 'Alex', 'Perry', 'Alex Perry', '403-730-0059', 'https://images.petshop.local/users/059.webp', 'CUSTOMER', 'CUSTOMER', NULL, 15, 1, 0), +(60, 'alex.powell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.powell@gmail.com', 'Alex', 'Powell', 'Alex Powell', '403-730-0060', 'https://images.petshop.local/users/060.webp', 'CUSTOMER', 'CUSTOMER', NULL, 4, 1, 0), +(61, 'alex.long', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.long@gmail.com', 'Alex', 'Long', 'Alex Long', '403-730-0061', 'https://images.petshop.local/users/061.webp', 'CUSTOMER', 'CUSTOMER', NULL, 13, 1, 0), +(62, 'alex.patterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.patterson@gmail.com', 'Alex', 'Patterson', 'Alex Patterson', '403-730-0062', 'https://images.petshop.local/users/062.webp', 'CUSTOMER', 'CUSTOMER', NULL, 26, 1, 0), +(63, 'alex.hughes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hughes@gmail.com', 'Alex', 'Hughes', 'Alex Hughes', '403-730-0063', 'https://images.petshop.local/users/063.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(64, 'alex.flores', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.flores@gmail.com', 'Alex', 'Flores', 'Alex Flores', '403-730-0064', 'https://images.petshop.local/users/064.webp', 'CUSTOMER', 'CUSTOMER', NULL, 9, 1, 0), +(65, 'alex.washington', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.washington@gmail.com', 'Alex', 'Washington', 'Alex Washington', '403-730-0065', 'https://images.petshop.local/users/065.webp', 'CUSTOMER', 'CUSTOMER', NULL, 22, 1, 0), +(66, 'alex.butler', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.butler@gmail.com', 'Alex', 'Butler', 'Alex Butler', '403-730-0066', 'https://images.petshop.local/users/066.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(67, 'alex.simmons', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.simmons@gmail.com', 'Alex', 'Simmons', 'Alex Simmons', '403-730-0067', 'https://images.petshop.local/users/067.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(68, 'alex.foster', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.foster@gmail.com', 'Alex', 'Foster', 'Alex Foster', '403-730-0068', 'https://images.petshop.local/users/068.webp', 'CUSTOMER', 'CUSTOMER', NULL, 17, 1, 0), +(69, 'alex.gonzales', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gonzales@gmail.com', 'Alex', 'Gonzales', 'Alex Gonzales', '403-730-0069', 'https://images.petshop.local/users/069.webp', 'CUSTOMER', 'CUSTOMER', NULL, 15, 1, 0), +(70, 'alex.bryant', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bryant@gmail.com', 'Alex', 'Bryant', 'Alex Bryant', '403-730-0070', 'https://images.petshop.local/users/070.webp', 'CUSTOMER', 'CUSTOMER', NULL, 19, 1, 0), +(71, 'alex.alexander', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.alexander@gmail.com', 'Alex', 'Alexander', 'Alex Alexander', '403-730-0071', 'https://images.petshop.local/users/071.webp', 'CUSTOMER', 'CUSTOMER', NULL, 13, 1, 0), +(72, 'alex.russell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.russell@gmail.com', 'Alex', 'Russell', 'Alex Russell', '403-730-0072', 'https://images.petshop.local/users/072.webp', 'CUSTOMER', 'CUSTOMER', NULL, 7, 1, 0), +(73, 'alex.griffin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.griffin@gmail.com', 'Alex', 'Griffin', 'Alex Griffin', '403-730-0073', 'https://images.petshop.local/users/073.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(74, 'alex.diaz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.diaz@gmail.com', 'Alex', 'Diaz', 'Alex Diaz', '403-730-0074', 'https://images.petshop.local/users/074.webp', 'CUSTOMER', 'CUSTOMER', NULL, 10, 1, 0), +(75, 'alex.hayes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hayes@gmail.com', 'Alex', 'Hayes', 'Alex Hayes', '403-730-0075', 'https://images.petshop.local/users/075.webp', 'CUSTOMER', 'CUSTOMER', NULL, 7, 1, 0), +(76, 'alex.myers', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.myers@gmail.com', 'Alex', 'Myers', 'Alex Myers', '403-730-0076', 'https://images.petshop.local/users/076.webp', 'CUSTOMER', 'CUSTOMER', NULL, 13, 1, 0), +(77, 'alex.ford', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ford@gmail.com', 'Alex', 'Ford', 'Alex Ford', '403-730-0077', 'https://images.petshop.local/users/077.webp', 'CUSTOMER', 'CUSTOMER', NULL, 13, 1, 0), +(78, 'alex.hamilton', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hamilton@gmail.com', 'Alex', 'Hamilton', 'Alex Hamilton', '403-730-0078', 'https://images.petshop.local/users/078.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(79, 'alex.graham', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.graham@gmail.com', 'Alex', 'Graham', 'Alex Graham', '403-730-0079', 'https://images.petshop.local/users/079.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(80, 'alex.sullivan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sullivan@gmail.com', 'Alex', 'Sullivan', 'Alex Sullivan', '403-730-0080', 'https://images.petshop.local/users/080.webp', 'CUSTOMER', 'CUSTOMER', NULL, 12, 1, 0), +(81, 'alex.wallace', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wallace@gmail.com', 'Alex', 'Wallace', 'Alex Wallace', '403-730-0081', 'https://images.petshop.local/users/081.webp', 'CUSTOMER', 'CUSTOMER', NULL, 11, 1, 0), +(82, 'alex.woods', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.woods@gmail.com', 'Alex', 'Woods', 'Alex Woods', '403-730-0082', 'https://images.petshop.local/users/082.webp', 'CUSTOMER', 'CUSTOMER', NULL, 17, 1, 0), +(83, 'alex.cole', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cole@gmail.com', 'Alex', 'Cole', 'Alex Cole', '403-730-0083', 'https://images.petshop.local/users/083.webp', 'CUSTOMER', 'CUSTOMER', NULL, 36, 1, 0), +(84, 'alex.west', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.west@gmail.com', 'Alex', 'West', 'Alex West', '403-730-0084', 'https://images.petshop.local/users/084.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(85, 'alex.jordan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jordan@gmail.com', 'Alex', 'Jordan', 'Alex Jordan', '403-730-0085', 'https://images.petshop.local/users/085.webp', 'CUSTOMER', 'CUSTOMER', NULL, 9, 1, 0), +(86, 'alex.owens', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.owens@gmail.com', 'Alex', 'Owens', 'Alex Owens', '403-730-0086', 'https://images.petshop.local/users/086.webp', 'CUSTOMER', 'CUSTOMER', NULL, 26, 1, 0), +(87, 'alex.reynolds', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reynolds@gmail.com', 'Alex', 'Reynolds', 'Alex Reynolds', '403-730-0087', 'https://images.petshop.local/users/087.webp', 'CUSTOMER', 'CUSTOMER', NULL, 3, 1, 0), +(88, 'alex.fisher', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.fisher@gmail.com', 'Alex', 'Fisher', 'Alex Fisher', '403-730-0088', 'https://images.petshop.local/users/088.webp', 'CUSTOMER', 'CUSTOMER', NULL, 11, 1, 0), +(89, 'alex.ellis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ellis@gmail.com', 'Alex', 'Ellis', 'Alex Ellis', '403-730-0089', 'https://images.petshop.local/users/089.webp', 'CUSTOMER', 'CUSTOMER', NULL, 30, 1, 0), +(90, 'alex.harrison', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.harrison@gmail.com', 'Alex', 'Harrison', 'Alex Harrison', '403-730-0090', 'https://images.petshop.local/users/090.webp', 'CUSTOMER', 'CUSTOMER', NULL, 16, 1, 0), +(91, 'alex.gibson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gibson@gmail.com', 'Alex', 'Gibson', 'Alex Gibson', '403-730-0091', 'https://images.petshop.local/users/091.webp', 'CUSTOMER', 'CUSTOMER', NULL, 9, 1, 0), +(92, 'alex.mcdonald', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mcdonald@gmail.com', 'Alex', 'Mcdonald', 'Alex Mcdonald', '403-730-0092', 'https://images.petshop.local/users/092.webp', 'CUSTOMER', 'CUSTOMER', NULL, 19, 1, 0), +(93, 'alex.cruz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cruz@gmail.com', 'Alex', 'Cruz', 'Alex Cruz', '403-730-0093', 'https://images.petshop.local/users/093.webp', 'CUSTOMER', 'CUSTOMER', NULL, 2, 1, 0), +(94, 'alex.marshall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.marshall@gmail.com', 'Alex', 'Marshall', 'Alex Marshall', '403-730-0094', 'https://images.petshop.local/users/094.webp', 'CUSTOMER', 'CUSTOMER', NULL, 5, 1, 0), +(95, 'alex.ortiz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ortiz@gmail.com', 'Alex', 'Ortiz', 'Alex Ortiz', '403-730-0095', 'https://images.petshop.local/users/095.webp', 'CUSTOMER', 'CUSTOMER', NULL, 30, 1, 0), +(96, 'alex.gomez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gomez@gmail.com', 'Alex', 'Gomez', 'Alex Gomez', '403-730-0096', 'https://images.petshop.local/users/096.webp', 'CUSTOMER', 'CUSTOMER', NULL, 6, 1, 0), +(97, 'alex.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murray@gmail.com', 'Alex', 'Murray', 'Alex Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', NULL, 4, 1, 0), +(98, 'alex.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.freeman@gmail.com', 'Alex', 'Freeman', 'Alex Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0), +(99, 'alex.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wells@gmail.com', 'Alex', 'Wells', 'Alex Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0), +(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', NULL, 0, 1, 0); + +INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES +(1, 'PetFood Inc', 'Robert', 'King', 'contact@petfood.com', '403-601-1001'), +(2, 'Toy World', 'Jennifer', 'Lee', 'sales@toyworld.com', '403-601-1002'), +(3, 'Pet Supplies Co', 'Kevin', 'White', 'info@petsupplies.com', '403-601-1003'), +(4, 'Animal Care Products', 'Nancy', 'Green', 'orders@animalcare.com', '403-601-1004'), +(5, 'Premium Pet Goods', 'Tom', 'Black', 'support@premiumpet.com', '403-601-1005'), +(6, 'Prairie Feeds', 'Lauren', 'Miles', 'hello@prairiefeeds.com', '403-601-1006'), +(7, 'Whisker Works', 'Darren', 'Cole', 'support@whiskerworks.com', '403-601-1007'), +(8, 'AquaLife Traders', 'Sonia', 'Bell', 'service@aqualife.com', '403-601-1008'), +(9, 'Feather & Finch', 'Maya', 'Stone', 'sales@featherfinch.com', '403-601-1009'), +(10, 'Habitat House', 'Riley', 'Ward', 'orders@habitathouse.com', '403-601-1010'), +(11, 'Trail Tails', 'Evan', 'Frost', 'contact@trailtails.com', '403-601-1011'), +(12, 'CalmPaws Health', 'Ivy', 'Brooks', 'care@calmpaws.com', '403-601-1012'); + +INSERT INTO category (categoryId, categoryName, categoryType) VALUES +(1, 'Dog Food', 'Product'), +(2, 'Cat Toys', 'Product'), +(3, 'Bird Supplies', 'Product'), +(4, 'Aquarium', 'Product'), +(5, 'Small Animals', 'Product'), +(6, 'Pet Health', 'Product'), +(7, 'Grooming Essentials', 'Product'), +(8, 'Habitats', 'Product'), +(9, 'Training & Travel', 'Product'), +(10, 'Treats & Chews', 'Product'); + +INSERT INTO service (serviceId, serviceName, serviceDesc, serviceDuration, servicePrice) VALUES +(1, 'Pet Grooming', 'Full grooming service for coat care and hygiene.', 60, 45.00), +(2, 'Nail Trimming', 'Quick nail trim for pets that need routine care.', 15, 12.00), +(3, 'Bath and Brush', 'Bathing and brushing service for shedding control.', 45, 34.00), +(4, 'Veterinary Checkup', 'General wellness check with basic health review.', 30, 80.00), +(5, 'Teeth Cleaning', 'Routine dental cleaning for eligible pets.', 50, 65.00), +(6, 'Wing Clipping', 'Safe wing trim for birds that require it.', 20, 18.00), +(7, 'Beak and Nail Care', 'Light beak and claw maintenance for birds.', 25, 22.00), +(8, 'Aquarium Health Check', 'Fish wellness and habitat consultation appointment.', 25, 28.00); + +INSERT INTO service_species (serviceId, species) VALUES +(1, 'Dog'), +(1, 'Cat'), +(1, 'Rabbit'), +(2, 'Dog'), +(2, 'Cat'), +(2, 'Rabbit'), +(2, 'Guinea Pig'), +(2, 'Hamster'), +(2, 'Bird'), +(3, 'Dog'), +(3, 'Cat'), +(3, 'Rabbit'), +(3, 'Guinea Pig'), +(4, 'Dog'), +(4, 'Cat'), +(4, 'Rabbit'), +(4, 'Bird'), +(4, 'Fish'), +(4, 'Hamster'), +(4, 'Guinea Pig'), +(5, 'Dog'), +(5, 'Cat'), +(5, 'Rabbit'), +(5, 'Guinea Pig'), +(5, 'Hamster'), +(6, 'Bird'), +(7, 'Bird'), +(8, 'Fish'); + +INSERT INTO product (prodId, prodName, prodPrice, categoryId, prodDesc, imageUrl) VALUES +(1, 'Premium Dog Food', 25.09, 1, 'Balanced nutrition for dogs. Premium Dog Food is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/001.webp'), +(2, 'Salmon Kibble Dog', 30.51, 1, 'Balanced nutrition for dogs. Salmon Kibble Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/002.webp'), +(3, 'Chicken Recipe Dog', 35.93, 1, 'Balanced nutrition for dogs. Chicken Recipe Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/003.webp'), +(4, 'Lamb Formula Dog', 41.36, 1, 'Balanced nutrition for dogs. Lamb Formula Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/004.webp'), +(5, 'Grain-Free Blend Dog', 46.78, 1, 'Balanced nutrition for dogs. Grain-Free Blend Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/005.webp'), +(6, 'Senior Dinner Dog', 52.20, 1, 'Balanced nutrition for dogs. Senior Dinner Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/006.webp'), +(7, 'Puppy Meal Dog', 57.62, 1, 'Balanced nutrition for dogs. Puppy Meal Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/007.webp'), +(8, 'Weight Control Bites Dog', 63.05, 1, 'Balanced nutrition for dogs. Weight Control Bites Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/008.webp'), +(9, 'High Energy Mix Dog', 68.47, 1, 'Balanced nutrition for dogs. High Energy Mix Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/009.webp'), +(10, 'Limited Ingredient Cuisine Dog', 73.89, 1, 'Balanced nutrition for dogs. Limited Ingredient Cuisine Dog is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/010.webp'), +(11, 'Catnip Toy', 7.49, 2, 'Playtime toy for cats. Catnip Toy is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/011.webp'), +(12, 'Feather Chaser Cat', 9.71, 2, 'Playtime toy for cats. Feather Chaser Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/012.webp'), +(13, 'Laser Teaser Cat', 11.93, 2, 'Playtime toy for cats. Laser Teaser Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/013.webp'), +(14, 'Tunnel Ball Cat', 14.16, 2, 'Playtime toy for cats. Tunnel Ball Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/014.webp'), +(15, 'Crinkle Set Cat', 16.38, 2, 'Playtime toy for cats. Crinkle Set Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/015.webp'), +(16, 'Wand Roller Cat', 18.60, 2, 'Playtime toy for cats. Wand Roller Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/016.webp'), +(17, 'Interactive Spinner Cat', 20.82, 2, 'Playtime toy for cats. Interactive Spinner Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/017.webp'), +(18, 'Bell Track Cat', 23.05, 2, 'Playtime toy for cats. Bell Track Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/018.webp'), +(19, 'Mouse Spring Cat', 25.27, 2, 'Playtime toy for cats. Mouse Spring Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/019.webp'), +(20, 'Puzzle Bundle Cat', 27.49, 2, 'Playtime toy for cats. Puzzle Bundle Cat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/020.webp'), +(21, 'Perch Bird Kit', 10.89, 3, 'Everyday bird care item. Perch Bird Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/021.webp'), +(22, 'Seed Accessory Bird', 15.25, 3, 'Everyday bird care item. Seed Accessory Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/022.webp'), +(23, 'Mirror Set Bird', 19.60, 3, 'Everyday bird care item. Mirror Set Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/023.webp'), +(24, 'Ladder Pack Bird', 23.96, 3, 'Everyday bird care item. Ladder Pack Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/024.webp'), +(25, 'Bell Supply Bird', 28.31, 3, 'Everyday bird care item. Bell Supply Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/025.webp'), +(26, 'Foraging Refill Bird', 32.67, 3, 'Everyday bird care item. Foraging Refill Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/026.webp'), +(27, 'Treat Stand Bird', 37.02, 3, 'Everyday bird care item. Treat Stand Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/027.webp'), +(28, 'Cuttlebone Mix Bird', 41.38, 3, 'Everyday bird care item. Cuttlebone Mix Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/028.webp'), +(29, 'Carrier Bundle Bird', 45.73, 3, 'Everyday bird care item. Carrier Bundle Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/029.webp'), +(30, 'Bath Support Bird', 50.09, 3, 'Everyday bird care item. Bath Support Bird is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/030.webp'), +(31, 'Nano Aquarium Kit', 21.29, 4, 'Aquarium and fish care supply. Nano Aquarium Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/031.webp'), +(32, 'Glass Accessory Aquarium', 34.00, 4, 'Aquarium and fish care supply. Glass Accessory Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/032.webp'), +(33, 'Filter Supply Aquarium', 46.71, 4, 'Aquarium and fish care supply. Filter Supply Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/033.webp'), +(34, 'Heater Filter Pack Aquarium', 59.42, 4, 'Aquarium and fish care supply. Heater Filter Pack Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/034.webp'), +(35, 'Water Tank Tool', 72.13, 4, 'Aquarium and fish care supply. Water Tank Tool is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/035.webp'), +(36, 'Coral Cleaner Aquarium', 84.85, 4, 'Aquarium and fish care supply. Coral Cleaner Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/036.webp'), +(37, 'Pebble Media Aquarium', 97.56, 4, 'Aquarium and fish care supply. Pebble Media Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/037.webp'), +(38, 'Plant Care Set Aquarium', 110.27, 4, 'Aquarium and fish care supply. Plant Care Set Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/038.webp'), +(39, 'Light Starter Aquarium', 122.98, 4, 'Aquarium and fish care supply. Light Starter Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/039.webp'), +(40, 'Pump System Aquarium', 135.69, 4, 'Aquarium and fish care supply. Pump System Aquarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/040.webp'), +(41, 'Hay Small Pet Kit', 11.04, 5, 'Care product for rabbits, hamsters, and guinea pigs. Hay Small Pet Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/041.webp'), +(42, 'Hideout Supply Small Pet', 16.86, 5, 'Care product for rabbits, hamsters, and guinea pigs. Hideout Supply Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/042.webp'), +(43, 'Chew Care Pack Small Pet', 22.68, 5, 'Care product for rabbits, hamsters, and guinea pigs. Chew Care Pack Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/043.webp'), +(44, 'Wheel Comfort Item Small Pet', 28.51, 5, 'Care product for rabbits, hamsters, and guinea pigs. Wheel Comfort Item Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/044.webp'), +(45, 'Bottle Exercise Toy Small Pet', 34.33, 5, 'Care product for rabbits, hamsters, and guinea pigs. Bottle Exercise Toy Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/045.webp'), +(46, 'Pellet Refill Small Pet', 40.15, 5, 'Care product for rabbits, hamsters, and guinea pigs. Pellet Refill Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/046.webp'), +(47, 'Tunnel Bundle Small Pet', 45.97, 5, 'Care product for rabbits, hamsters, and guinea pigs. Tunnel Bundle Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/047.webp'), +(48, 'Bedding Snack Small Pet', 51.80, 5, 'Care product for rabbits, hamsters, and guinea pigs. Bedding Snack Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/048.webp'), +(49, 'Play Starter Small Pet', 57.62, 5, 'Care product for rabbits, hamsters, and guinea pigs. Play Starter Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/049.webp'), +(50, 'Nest Accessory Small Pet', 63.44, 5, 'Care product for rabbits, hamsters, and guinea pigs. Nest Accessory Small Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/050.webp'), +(51, 'Calming Support Pet', 14.09, 6, 'General health support product. Calming Support Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/051.webp'), +(52, 'Joint Drops Pet', 18.62, 6, 'General health support product. Joint Drops Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/052.webp'), +(53, 'Digestive Chew Pet', 23.16, 6, 'General health support product. Digestive Chew Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/053.webp'), +(54, 'Skin Spray Pet', 27.69, 6, 'General health support product. Skin Spray Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/054.webp'), +(55, 'Ear Kit Pet', 32.22, 6, 'General health support product. Ear Kit Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/055.webp'), +(56, 'Dental Gel Pet', 36.76, 6, 'General health support product. Dental Gel Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/056.webp'), +(57, 'Vitamin Tabs Pet', 41.29, 6, 'General health support product. Vitamin Tabs Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/057.webp'), +(58, 'Recovery Wash Pet', 45.82, 6, 'General health support product. Recovery Wash Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/058.webp'), +(59, 'Hydration Powder Pet', 50.36, 6, 'General health support product. Hydration Powder Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/059.webp'), +(60, 'Wellness Formula Pet', 54.89, 6, 'General health support product. Wellness Formula Pet is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/060.webp'), +(61, 'Gentle Shampoo', 10.69, 7, 'Grooming essential for regular care. Gentle Shampoo is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/061.webp'), +(62, 'Deep Clean Brush', 13.09, 7, 'Grooming essential for regular care. Deep Clean Brush is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/062.webp'), +(63, 'Oatmeal Wipe', 15.49, 7, 'Grooming essential for regular care. Oatmeal Wipe is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/063.webp'), +(64, 'Deodorizing Conditioner', 17.89, 7, 'Grooming essential for regular care. Deodorizing Conditioner is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/064.webp'), +(65, 'Detangling Comb', 20.29, 7, 'Grooming essential for regular care. Detangling Comb is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/065.webp'), +(66, 'Soft Coat Mist', 22.69, 7, 'Grooming essential for regular care. Soft Coat Mist is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/066.webp'), +(67, 'Paw Foam', 25.09, 7, 'Grooming essential for regular care. Paw Foam is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/067.webp'), +(68, 'Fresh Towel', 27.49, 7, 'Grooming essential for regular care. Fresh Towel is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/068.webp'), +(69, 'Silky Rinse', 29.89, 7, 'Grooming essential for regular care. Silky Rinse is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/069.webp'), +(70, 'Quick Dry Balm', 32.29, 7, 'Grooming essential for regular care. Quick Dry Balm is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/070.webp'), +(71, 'Compact Habitat', 37.99, 8, 'Habitat or enclosure for pet comfort. Compact Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/071.webp'), +(72, 'Deluxe Crate', 53.99, 8, 'Habitat or enclosure for pet comfort. Deluxe Crate is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/072.webp'), +(73, 'Travel Carrier', 69.99, 8, 'Habitat or enclosure for pet comfort. Travel Carrier is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/073.webp'), +(74, 'Corner Enclosure', 85.99, 8, 'Habitat or enclosure for pet comfort. Corner Enclosure is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/074.webp'), +(75, 'Stacked Cage', 101.99, 8, 'Habitat or enclosure for pet comfort. Stacked Cage is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/075.webp'), +(76, 'Starter House Habitat', 117.99, 8, 'Habitat or enclosure for pet comfort. Starter House Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/076.webp'), +(77, 'Ventilated Stand Habitat', 133.99, 8, 'Habitat or enclosure for pet comfort. Ventilated Stand Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/077.webp'), +(78, 'Eco Loft Habitat', 149.99, 8, 'Habitat or enclosure for pet comfort. Eco Loft Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/078.webp'), +(79, 'Secure Playpen Habitat', 165.99, 8, 'Habitat or enclosure for pet comfort. Secure Playpen Habitat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/079.webp'), +(80, 'Open-Air Terrarium', 181.99, 8, 'Habitat or enclosure for pet comfort. Open-Air Terrarium is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/080.webp'), +(81, 'Clicker Kit', 17.99, 9, 'Training and travel accessory. Clicker Kit is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/081.webp'), +(82, 'Harness Lead', 25.10, 9, 'Training and travel accessory. Harness Lead is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/082.webp'), +(83, 'Seatbelt Set', 32.21, 9, 'Training and travel accessory. Seatbelt Set is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/083.webp'), +(84, 'Travel Bag', 39.32, 9, 'Training and travel accessory. Travel Bag is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/084.webp'), +(85, 'Training Accessory', 46.43, 9, 'Training and travel accessory. Training Accessory is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/085.webp'), +(86, 'Recall Clip', 53.55, 9, 'Training and travel accessory. Recall Clip is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/086.webp'), +(87, 'Walking Tag', 60.66, 9, 'Training and travel accessory. Walking Tag is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/087.webp'), +(88, 'Crate Pack', 67.77, 9, 'Training and travel accessory. Crate Pack is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/088.webp'), +(89, 'Carrier Mat', 74.88, 9, 'Training and travel accessory. Carrier Mat is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/089.webp'), +(90, 'Adventure Guide', 81.99, 9, 'Training and travel accessory. Adventure Guide is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/090.webp'), +(91, 'Chicken Treats', 7.44, 10, 'Treat or chew for reward-based care. Chicken Treats is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/091.webp'), +(92, 'Duck Chews', 9.17, 10, 'Treat or chew for reward-based care. Duck Chews is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/092.webp'), +(93, 'Salmon Bites', 10.91, 10, 'Treat or chew for reward-based care. Salmon Bites is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/093.webp'), +(94, 'Pumpkin Snacks', 12.64, 10, 'Treat or chew for reward-based care. Pumpkin Snacks is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/094.webp'), +(95, 'Crunchy Rewards', 14.37, 10, 'Treat or chew for reward-based care. Crunchy Rewards is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/095.webp'), +(96, 'Soft-Bake Jerky', 16.11, 10, 'Treat or chew for reward-based care. Soft-Bake Jerky is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/096.webp'), +(97, 'Dental Cubes', 17.84, 10, 'Treat or chew for reward-based care. Dental Cubes is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/097.webp'), +(98, 'Mini Crisps', 19.57, 10, 'Treat or chew for reward-based care. Mini Crisps is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/098.webp'), +(99, 'Natural Morsels', 21.31, 10, 'Treat or chew for reward-based care. Natural Morsels is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/099.webp'), +(100, 'Freeze-Dried Biscuits', 23.04, 10, 'Treat or chew for reward-based care. Freeze-Dried Biscuits is stocked in all locations and priced for daily retail.', 'https://images.petshop.local/products/100.webp'); + +INSERT INTO productSupplier (supId, prodId, cost) VALUES +(1, 1, 14.55), +(1, 2, 18.61), +(6, 2, 19.91), +(1, 3, 23.00), +(1, 4, 27.71), +(6, 4, 29.10), +(1, 5, 32.75), +(1, 6, 38.11), +(6, 6, 39.25), +(1, 7, 31.69), +(1, 8, 36.57), +(6, 8, 39.13), +(1, 9, 41.77), +(1, 10, 47.29), +(6, 10, 49.65), +(2, 11, 5.02), +(2, 12, 6.80), +(7, 12, 7.00), +(2, 13, 8.71), +(2, 14, 7.79), +(7, 14, 8.34), +(2, 15, 9.50), +(2, 16, 11.35), +(7, 16, 11.92), +(2, 17, 13.32), +(2, 18, 15.44), +(7, 18, 15.90), +(2, 19, 17.69), +(2, 20, 20.07), +(7, 20, 21.47), +(9, 21, 5.99), +(9, 22, 8.85), +(3, 22, 9.29), +(9, 23, 11.96), +(9, 24, 15.33), +(3, 24, 15.79), +(9, 25, 18.97), +(9, 26, 22.87), +(3, 26, 24.47), +(9, 27, 27.02), +(9, 28, 22.76), +(3, 28, 23.90), +(9, 29, 26.52), +(9, 30, 30.55), +(3, 30, 31.47), +(8, 31, 13.63), +(8, 32, 22.78), +(3, 32, 24.37), +(8, 33, 32.70), +(8, 34, 43.38), +(3, 34, 45.55), +(8, 35, 39.67), +(8, 36, 49.21), +(3, 36, 50.69), +(8, 37, 59.51), +(8, 38, 70.57), +(3, 38, 75.51), +(8, 39, 82.40), +(8, 40, 94.98), +(3, 40, 99.73), +(3, 41, 8.06), +(3, 42, 9.27), +(10, 42, 9.55), +(3, 43, 13.15), +(3, 44, 17.39), +(10, 44, 18.61), +(3, 45, 21.97), +(3, 46, 26.90), +(10, 46, 28.25), +(3, 47, 32.18), +(3, 48, 37.81), +(10, 48, 38.94), +(3, 49, 31.69), +(3, 50, 36.80), +(10, 50, 39.38), +(12, 51, 8.59), +(12, 52, 11.92), +(4, 52, 12.52), +(12, 53, 15.52), +(12, 54, 19.38), +(4, 54, 19.96), +(12, 55, 23.52), +(12, 56, 20.22), +(4, 56, 21.64), +(12, 57, 23.95), +(12, 58, 27.95), +(4, 58, 29.35), +(12, 59, 32.23), +(12, 60, 36.78), +(4, 60, 37.88), +(4, 61, 7.48), +(4, 62, 9.56), +(5, 62, 10.23), +(4, 63, 8.52), +(4, 64, 10.38), +(5, 64, 10.90), +(4, 65, 12.38), +(4, 66, 14.52), +(5, 66, 14.96), +(4, 67, 16.81), +(4, 68, 19.24), +(5, 68, 20.59), +(4, 69, 21.82), +(4, 70, 17.76), +(5, 70, 18.65), +(10, 71, 22.03), +(10, 72, 32.93), +(3, 72, 33.92), +(10, 73, 44.79), +(10, 74, 57.61), +(3, 74, 61.64), +(10, 75, 71.39), +(10, 76, 86.13), +(3, 76, 90.44), +(10, 77, 73.69), +(10, 78, 86.99), +(3, 78, 89.60), +(10, 79, 101.25), +(10, 80, 116.47), +(3, 80, 124.62), +(11, 81, 12.05), +(11, 82, 17.57), +(5, 82, 18.45), +(11, 83, 23.51), +(11, 84, 21.63), +(5, 84, 22.28), +(11, 85, 26.93), +(11, 86, 32.67), +(5, 86, 34.96), +(11, 87, 38.82), +(11, 88, 45.41), +(5, 88, 47.68), +(11, 89, 52.42), +(11, 90, 59.85), +(5, 90, 61.65), +(1, 91, 4.09), +(1, 92, 5.32), +(5, 92, 5.69), +(1, 93, 6.66), +(1, 94, 8.09), +(5, 94, 8.49), +(1, 95, 9.63), +(1, 96, 11.28), +(5, 96, 11.62), +(1, 97, 13.02), +(1, 98, 10.76), +(5, 98, 11.51), +(1, 99, 12.36), +(1, 100, 14.05), +(5, 100, 14.75); + +INSERT INTO inventory (inventoryId, storeId, prodId, quantity) VALUES +(1, 1, 1, 52), +(2, 1, 2, 59), +(3, 1, 3, 66), +(4, 1, 4, 73), +(5, 1, 5, 80), +(6, 1, 6, 87), +(7, 1, 7, 29), +(8, 1, 8, 36), +(9, 1, 9, 43), +(10, 1, 10, 50), +(11, 1, 11, 37), +(12, 1, 12, 44), +(13, 1, 13, 51), +(14, 1, 14, 58), +(15, 1, 15, 65), +(16, 1, 16, 72), +(17, 1, 17, 14), +(18, 1, 18, 21), +(19, 1, 19, 28), +(20, 1, 20, 35), +(21, 1, 21, 42), +(22, 1, 22, 49), +(23, 1, 23, 56), +(24, 1, 24, 63), +(25, 1, 25, 70), +(26, 1, 26, 12), +(27, 1, 27, 19), +(28, 1, 28, 26), +(29, 1, 29, 33), +(30, 1, 30, 40), +(31, 1, 31, 8), +(32, 1, 32, 13), +(33, 1, 33, 18), +(34, 1, 34, 5), +(35, 1, 35, 10), +(36, 1, 36, 15), +(37, 1, 37, 20), +(38, 1, 38, 7), +(39, 1, 39, 12), +(40, 1, 40, 17), +(41, 1, 41, 52), +(42, 1, 42, 59), +(43, 1, 43, 66), +(44, 1, 44, 8), +(45, 1, 45, 15), +(46, 1, 46, 22), +(47, 1, 47, 29), +(48, 1, 48, 36), +(49, 1, 49, 43), +(50, 1, 50, 50), +(51, 1, 51, 57), +(52, 1, 52, 64), +(53, 1, 53, 71), +(54, 1, 54, 13), +(55, 1, 55, 20), +(56, 1, 56, 27), +(57, 1, 57, 34), +(58, 1, 58, 41), +(59, 1, 59, 48), +(60, 1, 60, 55), +(61, 1, 61, 62), +(62, 1, 62, 69), +(63, 1, 63, 11), +(64, 1, 64, 18), +(65, 1, 65, 25), +(66, 1, 66, 32), +(67, 1, 67, 39), +(68, 1, 68, 46), +(69, 1, 69, 53), +(70, 1, 70, 60), +(71, 1, 71, 10), +(72, 1, 72, 15), +(73, 1, 73, 20), +(74, 1, 74, 7), +(75, 1, 75, 12), +(76, 1, 76, 17), +(77, 1, 77, 4), +(78, 1, 78, 9), +(79, 1, 79, 14), +(80, 1, 80, 19), +(81, 1, 81, 72), +(82, 1, 82, 14), +(83, 1, 83, 21), +(84, 1, 84, 28), +(85, 1, 85, 35), +(86, 1, 86, 42), +(87, 1, 87, 49), +(88, 1, 88, 56), +(89, 1, 89, 63), +(90, 1, 90, 70), +(91, 1, 91, 32), +(92, 1, 92, 39), +(93, 1, 93, 46), +(94, 1, 94, 53), +(95, 1, 95, 60), +(96, 1, 96, 67), +(97, 1, 97, 74), +(98, 1, 98, 81), +(99, 1, 99, 88), +(100, 1, 100, 30), +(101, 2, 1, 69), +(102, 2, 2, 76), +(103, 2, 3, 83), +(104, 2, 4, 90), +(105, 2, 5, 32), +(106, 2, 6, 39), +(107, 2, 7, 46), +(108, 2, 8, 53), +(109, 2, 9, 60), +(110, 2, 10, 67), +(111, 2, 11, 54), +(112, 2, 12, 61), +(113, 2, 13, 68), +(114, 2, 14, 10), +(115, 2, 15, 17), +(116, 2, 16, 24), +(117, 2, 17, 31), +(118, 2, 18, 38), +(119, 2, 19, 45), +(120, 2, 20, 52), +(121, 2, 21, 59), +(122, 2, 22, 66), +(123, 2, 23, 8), +(124, 2, 24, 15), +(125, 2, 25, 22), +(126, 2, 26, 29), +(127, 2, 27, 36), +(128, 2, 28, 43), +(129, 2, 29, 50), +(130, 2, 30, 57), +(131, 2, 31, 19), +(132, 2, 32, 6), +(133, 2, 33, 11), +(134, 2, 34, 16), +(135, 2, 35, 21), +(136, 2, 36, 8), +(137, 2, 37, 13), +(138, 2, 38, 18), +(139, 2, 39, 5), +(140, 2, 40, 10), +(141, 2, 41, 69), +(142, 2, 42, 11), +(143, 2, 43, 18), +(144, 2, 44, 25), +(145, 2, 45, 32), +(146, 2, 46, 39), +(147, 2, 47, 46), +(148, 2, 48, 53), +(149, 2, 49, 60), +(150, 2, 50, 67), +(151, 2, 51, 9), +(152, 2, 52, 16), +(153, 2, 53, 23), +(154, 2, 54, 30), +(155, 2, 55, 37), +(156, 2, 56, 44), +(157, 2, 57, 51), +(158, 2, 58, 58), +(159, 2, 59, 65), +(160, 2, 60, 72), +(161, 2, 61, 14), +(162, 2, 62, 21), +(163, 2, 63, 28), +(164, 2, 64, 35), +(165, 2, 65, 42), +(166, 2, 66, 49), +(167, 2, 67, 56), +(168, 2, 68, 63), +(169, 2, 69, 70), +(170, 2, 70, 12), +(171, 2, 71, 21), +(172, 2, 72, 8), +(173, 2, 73, 13), +(174, 2, 74, 18), +(175, 2, 75, 5), +(176, 2, 76, 10), +(177, 2, 77, 15), +(178, 2, 78, 20), +(179, 2, 79, 7), +(180, 2, 80, 12), +(181, 2, 81, 24), +(182, 2, 82, 31), +(183, 2, 83, 38), +(184, 2, 84, 45), +(185, 2, 85, 52), +(186, 2, 86, 59), +(187, 2, 87, 66), +(188, 2, 88, 8), +(189, 2, 89, 15), +(190, 2, 90, 22), +(191, 2, 91, 49), +(192, 2, 92, 56), +(193, 2, 93, 63), +(194, 2, 94, 70), +(195, 2, 95, 77), +(196, 2, 96, 84), +(197, 2, 97, 91), +(198, 2, 98, 33), +(199, 2, 99, 40), +(200, 2, 100, 47), +(201, 3, 1, 86), +(202, 3, 2, 28), +(203, 3, 3, 35), +(204, 3, 4, 42), +(205, 3, 5, 49), +(206, 3, 6, 56), +(207, 3, 7, 63), +(208, 3, 8, 70), +(209, 3, 9, 77), +(210, 3, 10, 84), +(211, 3, 11, 71), +(212, 3, 12, 13), +(213, 3, 13, 20), +(214, 3, 14, 27), +(215, 3, 15, 34), +(216, 3, 16, 41), +(217, 3, 17, 48), +(218, 3, 18, 55), +(219, 3, 19, 62), +(220, 3, 20, 69), +(221, 3, 21, 11), +(222, 3, 22, 18), +(223, 3, 23, 25), +(224, 3, 24, 32), +(225, 3, 25, 39), +(226, 3, 26, 46), +(227, 3, 27, 53), +(228, 3, 28, 60), +(229, 3, 29, 67), +(230, 3, 30, 9), +(231, 3, 31, 12), +(232, 3, 32, 17), +(233, 3, 33, 4), +(234, 3, 34, 9), +(235, 3, 35, 14), +(236, 3, 36, 19), +(237, 3, 37, 6), +(238, 3, 38, 11), +(239, 3, 39, 16), +(240, 3, 40, 21), +(241, 3, 41, 21), +(242, 3, 42, 28), +(243, 3, 43, 35), +(244, 3, 44, 42), +(245, 3, 45, 49), +(246, 3, 46, 56), +(247, 3, 47, 63), +(248, 3, 48, 70), +(249, 3, 49, 12), +(250, 3, 50, 19), +(251, 3, 51, 26), +(252, 3, 52, 33), +(253, 3, 53, 40), +(254, 3, 54, 47), +(255, 3, 55, 54), +(256, 3, 56, 61), +(257, 3, 57, 68), +(258, 3, 58, 10), +(259, 3, 59, 17), +(260, 3, 60, 24), +(261, 3, 61, 31), +(262, 3, 62, 38), +(263, 3, 63, 45), +(264, 3, 64, 52), +(265, 3, 65, 59), +(266, 3, 66, 66), +(267, 3, 67, 8), +(268, 3, 68, 15), +(269, 3, 69, 22), +(270, 3, 70, 29), +(271, 3, 71, 14), +(272, 3, 72, 19), +(273, 3, 73, 6), +(274, 3, 74, 11), +(275, 3, 75, 16), +(276, 3, 76, 21), +(277, 3, 77, 8), +(278, 3, 78, 13), +(279, 3, 79, 18), +(280, 3, 80, 5), +(281, 3, 81, 41), +(282, 3, 82, 48), +(283, 3, 83, 55), +(284, 3, 84, 62), +(285, 3, 85, 69), +(286, 3, 86, 11), +(287, 3, 87, 18), +(288, 3, 88, 25), +(289, 3, 89, 32), +(290, 3, 90, 39), +(291, 3, 91, 66), +(292, 3, 92, 73), +(293, 3, 93, 80), +(294, 3, 94, 87), +(295, 3, 95, 29), +(296, 3, 96, 36), +(297, 3, 97, 43), +(298, 3, 98, 50), +(299, 3, 99, 57), +(300, 3, 100, 64); + +INSERT INTO purchaseOrder (purchaseOrderId, supId, storeId, orderDate, status) VALUES +(1, 3, 1, '2026-01-06', 'RECEIVED'), +(2, 4, 1, '2026-01-13', 'RECEIVED'), +(3, 5, 1, '2026-01-20', 'RECEIVED'), +(4, 4, 2, '2026-01-07', 'RECEIVED'), +(5, 5, 2, '2026-01-14', 'RECEIVED'), +(6, 6, 2, '2026-01-21', 'RECEIVED'), +(7, 5, 3, '2026-01-08', 'RECEIVED'), +(8, 6, 3, '2026-01-15', 'RECEIVED'), +(9, 7, 3, '2026-01-22', 'RECEIVED'), +(10, 4, 1, '2026-02-06', 'RECEIVED'), +(11, 5, 1, '2026-02-13', 'RECEIVED'), +(12, 6, 1, '2026-02-20', 'RECEIVED'), +(13, 5, 2, '2026-02-07', 'RECEIVED'), +(14, 6, 2, '2026-02-14', 'RECEIVED'), +(15, 7, 2, '2026-02-21', 'RECEIVED'), +(16, 6, 3, '2026-02-08', 'RECEIVED'), +(17, 7, 3, '2026-02-15', 'RECEIVED'), +(18, 8, 3, '2026-02-22', 'RECEIVED'), +(19, 5, 1, '2026-03-06', 'RECEIVED'), +(20, 6, 1, '2026-03-13', 'RECEIVED'), +(21, 7, 1, '2026-03-20', 'RECEIVED'), +(22, 6, 2, '2026-03-07', 'RECEIVED'), +(23, 7, 2, '2026-03-14', 'RECEIVED'), +(24, 8, 2, '2026-03-21', 'RECEIVED'), +(25, 7, 3, '2026-03-08', 'RECEIVED'), +(26, 8, 3, '2026-03-15', 'RECEIVED'), +(27, 9, 3, '2026-03-22', 'RECEIVED'), +(28, 6, 1, '2026-04-06', 'PENDING'), +(29, 7, 1, '2026-04-13', 'RECEIVED'), +(30, 8, 1, '2026-04-20', 'PLACED'), +(31, 7, 2, '2026-04-07', 'RECEIVED'), +(32, 8, 2, '2026-04-14', 'PLACED'), +(33, 9, 2, '2026-04-21', 'PENDING'), +(34, 8, 3, '2026-04-08', 'PLACED'), +(35, 9, 3, '2026-04-15', 'PENDING'), +(36, 10, 3, '2026-04-22', 'RECEIVED'); + +INSERT INTO coupon (couponId, couponCode, discountType, discountValue, minOrderAmount, active, startsAt, endsAt, usageLimit) VALUES +(1, 'NOCODE', 'FIXED', 0.00, 0.00, 1, NULL, NULL, NULL), +(2, 'WELCOME10', 'PERCENT', 10.00, 50.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 300), +(3, 'TREAT5', 'FIXED', 5.00, 25.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 500), +(4, 'GROOM15', 'PERCENT', 15.00, 60.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 200), +(5, 'FISHCARE8', 'FIXED', 8.00, 40.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(6, 'BIRD10', 'PERCENT', 10.00, 30.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 150), +(7, 'SPRING12', 'PERCENT', 12.00, 75.00, 1, '2026-03-01 00:00:00', '2026-05-31 23:59:59', 180), +(8, 'NEWPET20', 'FIXED', 20.00, 100.00, 1, '2026-01-01 00:00:00', '2026-12-31 23:59:59', 120); + +INSERT INTO pet (petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, ownerUserId, storeId) VALUES +(1, 'Buddy', 'Dog', 'Corgi', 2, 'Available', 466.80, 'https://images.petshop.local/pets/001.webp', NULL, 1), +(2, 'Milo', 'Dog', 'Beagle', 3, 'Available', 513.60, 'https://images.petshop.local/pets/002.webp', NULL, 1), +(3, 'Charlie', 'Dog', 'Husky', 4, 'Available', 560.40, 'https://images.petshop.local/pets/003.webp', NULL, 1), +(4, 'Luna', 'Cat', 'Persian', 5, 'Available', 395.20, 'https://images.petshop.local/pets/004.webp', NULL, 1), +(5, 'Max', 'Cat', 'Siamese', 6, 'Available', 429.00, 'https://images.petshop.local/pets/005.webp', NULL, 1), +(6, 'Bella', 'Rabbit', 'Dutch', 1, 'Available', 211.40, 'https://images.petshop.local/pets/006.webp', NULL, 1), +(7, 'Rocky', 'Bird', 'Budgie', 2, 'Available', 169.20, 'https://images.petshop.local/pets/007.webp', NULL, 1), +(8, 'Daisy', 'Fish', 'Guppy', 3, 'Available', 16.20, 'https://images.petshop.local/pets/008.webp', NULL, 1), +(9, 'Cooper', 'Hamster', 'Roborovski', 4, 'Available', 30.10, 'https://images.petshop.local/pets/009.webp', NULL, 1), +(10, 'Ruby', 'Guinea Pig', 'Abyssinian', 5, 'Available', 53.50, 'https://images.petshop.local/pets/010.webp', NULL, 1), +(11, 'Tucker', 'Dog', 'Border Collie', 6, 'Available', 574.80, 'https://images.petshop.local/pets/011.webp', NULL, 1), +(12, 'Rosie', 'Cat', 'Maine Coon', 1, 'Available', 405.60, 'https://images.petshop.local/pets/012.webp', NULL, 1), +(13, 'Bear', 'Dog', 'Labrador', 2, 'Available', 668.40, 'https://images.petshop.local/pets/013.webp', NULL, 2), +(14, 'Maggie', 'Dog', 'Golden Retriever', 3, 'Available', 715.20, 'https://images.petshop.local/pets/014.webp', NULL, 2), +(15, 'Leo', 'Dog', 'Husky', 4, 'Available', 762.00, 'https://images.petshop.local/pets/015.webp', NULL, 2), +(16, 'Zoey', 'Cat', 'Scottish Fold', 5, 'Available', 280.80, 'https://images.petshop.local/pets/016.webp', NULL, 2), +(17, 'Oliver', 'Cat', 'Siamese', 6, 'Available', 314.60, 'https://images.petshop.local/pets/017.webp', NULL, 2), +(18, 'Lola', 'Rabbit', 'Netherland Dwarf', 1, 'Available', 154.20, 'https://images.petshop.local/pets/018.webp', NULL, 2), +(19, 'Buster', 'Bird', 'Budgie', 2, 'Available', 116.40, 'https://images.petshop.local/pets/019.webp', NULL, 2), +(20, 'Sadie', 'Fish', 'Tetra', 3, 'Available', 33.00, 'https://images.petshop.local/pets/020.webp', NULL, 2), +(21, 'Toby', 'Hamster', 'Dwarf', 4, 'Available', 46.90, 'https://images.petshop.local/pets/021.webp', NULL, 2), +(22, 'Cleo', 'Guinea Pig', 'Abyssinian', 5, 'Available', 78.70, 'https://images.petshop.local/pets/022.webp', NULL, 2), +(23, 'Harley', 'Dog', 'Boxer', 6, 'Available', 776.40, 'https://images.petshop.local/pets/023.webp', NULL, 2), +(24, 'Mocha', 'Cat', 'Siamese', 1, 'Available', 291.20, 'https://images.petshop.local/pets/024.webp', NULL, 2), +(25, 'Rex', 'Dog', 'Poodle', 2, 'Available', 510.00, 'https://images.petshop.local/pets/025.webp', NULL, 3), +(26, 'Willow', 'Dog', 'Boxer', 3, 'Available', 556.80, 'https://images.petshop.local/pets/026.webp', NULL, 3), +(27, 'Gizmo', 'Dog', 'Labrador', 4, 'Available', 603.60, 'https://images.petshop.local/pets/027.webp', NULL, 3), +(28, 'Nala', 'Cat', 'Calico', 5, 'Available', 426.40, 'https://images.petshop.local/pets/028.webp', NULL, 3), +(29, 'Duke', 'Cat', 'Calico', 6, 'Available', 460.20, 'https://images.petshop.local/pets/029.webp', NULL, 3), +(30, 'Misty', 'Rabbit', 'Lionhead', 1, 'Available', 227.00, 'https://images.petshop.local/pets/030.webp', NULL, 3), +(31, 'Ace', 'Bird', 'Budgie', 2, 'Available', 63.60, 'https://images.petshop.local/pets/031.webp', NULL, 3), +(32, 'Pepper', 'Fish', 'Goldfish', 3, 'Available', 19.80, 'https://images.petshop.local/pets/032.webp', NULL, 3), +(33, 'Coco', 'Hamster', 'Syrian', 4, 'Available', 33.70, 'https://images.petshop.local/pets/033.webp', NULL, 3), +(34, 'Finn', 'Guinea Pig', 'Peruvian', 5, 'Available', 58.90, 'https://images.petshop.local/pets/034.webp', NULL, 3), +(35, 'Shadow', 'Dog', 'Beagle', 6, 'Available', 618.00, 'https://images.petshop.local/pets/035.webp', NULL, 3), +(36, 'Kitty', 'Cat', 'British Shorthair', 1, 'Available', 436.80, 'https://images.petshop.local/pets/036.webp', NULL, 3), +(37, 'Bruno', 'Bird', 'Cockatiel', 6, 'Adopted', 94.80, 'https://images.petshop.local/pets/037.webp', 16, 1), +(38, 'Snowball', 'Fish', 'Betta', 8, 'Adopted', 28.80, 'https://images.petshop.local/pets/038.webp', 17, 2), +(39, 'Zeus', 'Fish', 'Guppy', 2, 'Adopted', 33.90, 'https://images.petshop.local/pets/039.webp', 18, 3), +(40, 'Biscuit', 'Fish', 'Goldfish', 4, 'Adopted', 39.00, 'https://images.petshop.local/pets/040.webp', 19, 1), +(41, 'Patches', 'Dog', 'Boxer', 6, 'Adopted', 769.20, 'https://images.petshop.local/pets/041.webp', 20, 2), +(42, 'Scout', 'Fish', 'Goldfish', 8, 'Adopted', 19.20, 'https://images.petshop.local/pets/042.webp', 21, 3), +(43, 'Mittens', 'Rabbit', 'Holland Lop', 2, 'Adopted', 150.30, 'https://images.petshop.local/pets/043.webp', 22, 1), +(44, 'Thor', 'Fish', 'Betta', 4, 'Adopted', 29.40, 'https://images.petshop.local/pets/044.webp', 23, 2), +(45, 'Whiskers', 'Fish', 'Betta', 6, 'Adopted', 34.50, 'https://images.petshop.local/pets/045.webp', 24, 3), +(46, 'Goldie', 'Fish', 'Goldfish', 8, 'Adopted', 39.60, 'https://images.petshop.local/pets/046.webp', 25, 1), +(47, 'Midnight', 'Bird', 'Parakeet', 2, 'Adopted', 178.80, 'https://images.petshop.local/pets/047.webp', 26, 2), +(48, 'Storm', 'Bird', 'Canary', 4, 'Adopted', 79.20, 'https://images.petshop.local/pets/048.webp', 27, 3), +(49, 'Peanut', 'Bird', 'Parakeet', 6, 'Adopted', 99.60, 'https://images.petshop.local/pets/049.webp', 28, 1), +(50, 'Daisy', 'Bird', 'Canary', 8, 'Adopted', 120.00, 'https://images.petshop.local/pets/050.webp', 29, 2), +(51, 'Cleo', 'Rabbit', 'Netherland Dwarf', 2, 'Adopted', 197.10, 'https://images.petshop.local/pets/051.webp', 30, 3), +(52, 'Sunny', 'Cat', 'Maine Coon', 4, 'Adopted', 478.40, 'https://images.petshop.local/pets/052.webp', 31, 1), +(53, 'Maple', 'Dog', 'Boxer', 6, 'Adopted', 423.60, 'https://images.petshop.local/pets/053.webp', 32, 2), +(54, 'Nova', 'Rabbit', 'Dutch', 8, 'Adopted', 133.40, 'https://images.petshop.local/pets/054.webp', 33, 3), +(55, 'Piper', 'Dog', 'Shih Tzu', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/055.webp', 34, NULL), +(56, 'Hazel', 'Cat', 'Bengal', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/056.webp', 35, NULL), +(57, 'Jasper', 'Rabbit', 'Lionhead', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/057.webp', 36, NULL), +(58, 'Remy', 'Bird', 'Canary', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/058.webp', 37, NULL), +(59, 'Archie', 'Fish', 'Tetra', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/059.webp', 38, NULL), +(60, 'Skye', 'Hamster', 'Syrian', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/060.webp', 39, NULL), +(61, 'Otis', 'Guinea Pig', 'Abyssinian', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/061.webp', 40, NULL), +(62, 'Marley', 'Dog', 'Border Collie', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/062.webp', 41, NULL), +(63, 'Blue', 'Cat', 'Scottish Fold', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/063.webp', 42, NULL), +(64, 'Honey', 'Rabbit', 'Netherland Dwarf', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/064.webp', 43, NULL), +(65, 'Mochi', 'Bird', 'Canary', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/065.webp', 44, NULL), +(66, 'Kiki', 'Fish', 'Goldfish', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/066.webp', 45, NULL), +(67, 'River', 'Hamster', 'Dwarf', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/067.webp', 46, NULL), +(68, 'Bowie', 'Guinea Pig', 'American', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/068.webp', 47, NULL), +(69, 'Sage', 'Dog', 'Labrador', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/069.webp', 48, NULL), +(70, 'Echo', 'Cat', 'Siamese', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/070.webp', 49, NULL), +(71, 'Poppy', 'Rabbit', 'Dutch', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/071.webp', 50, NULL), +(72, 'Juniper', 'Bird', 'Parakeet', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/072.webp', 51, NULL), +(73, 'Winston', 'Fish', 'Guppy', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/073.webp', 52, NULL), +(74, 'Freya', 'Hamster', 'Dwarf', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/074.webp', 53, NULL), +(75, 'Finnley', 'Guinea Pig', 'Peruvian', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/075.webp', 54, NULL), +(76, 'Louie', 'Dog', 'Corgi', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/076.webp', 55, NULL), +(77, 'Ivy', 'Cat', 'Calico', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/077.webp', 56, NULL), +(78, 'Binx', 'Rabbit', 'Lionhead', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/078.webp', 57, NULL), +(79, 'Suki', 'Bird', 'Parakeet', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/079.webp', 58, NULL), +(80, 'Mabel', 'Fish', 'Molly', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/080.webp', 59, NULL), +(81, 'Rolo', 'Hamster', 'Syrian', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/081.webp', 60, NULL), +(82, 'Clover', 'Guinea Pig', 'Abyssinian', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/082.webp', 61, NULL), +(83, 'Frankie', 'Dog', 'German Shepherd', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/083.webp', 62, NULL), +(84, 'Tilly', 'Cat', 'Tabby', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/084.webp', 63, NULL), +(85, 'Rory', 'Rabbit', 'Holland Lop', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/085.webp', 64, NULL), +(86, 'Gus', 'Bird', 'Budgie', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/086.webp', 65, NULL), +(87, 'Peaches', 'Fish', 'Guppy', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/087.webp', 66, NULL), +(88, 'Indie', 'Hamster', 'Roborovski', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/088.webp', 67, NULL), +(89, 'Minnie', 'Guinea Pig', 'Peruvian', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/089.webp', 68, NULL), +(90, 'Koda', 'Dog', 'Shih Tzu', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/090.webp', 69, NULL), +(91, 'Mango', 'Cat', 'British Shorthair', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/091.webp', 70, NULL), +(92, 'Pearl', 'Rabbit', 'Lionhead', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/092.webp', 71, NULL), +(93, 'Onyx', 'Bird', 'Canary', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/093.webp', 72, NULL), +(94, 'Pumpkin', 'Fish', 'Betta', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/094.webp', 73, NULL), +(95, 'Nori', 'Hamster', 'Dwarf', 4, 'Owned', 0.00, 'https://images.petshop.local/pets/095.webp', 74, NULL), +(96, 'Cosmo', 'Guinea Pig', 'American', 6, 'Owned', 0.00, 'https://images.petshop.local/pets/096.webp', 75, NULL), +(97, 'Ziggy', 'Dog', 'Beagle', 8, 'Owned', 0.00, 'https://images.petshop.local/pets/097.webp', 76, NULL), +(98, 'Bean', 'Cat', 'Calico', 10, 'Owned', 0.00, 'https://images.petshop.local/pets/098.webp', 77, NULL), +(99, 'Flora', 'Rabbit', 'Holland Lop', 12, 'Owned', 0.00, 'https://images.petshop.local/pets/099.webp', 78, NULL), +(100, 'Comet', 'Bird', 'Lovebird', 2, 'Owned', 0.00, 'https://images.petshop.local/pets/100.webp', 79, NULL); + +INSERT INTO appointment (appointmentId, serviceId, petId, customerId, storeId, employeeId, appointmentDate, appointmentTime, appointmentStatus) VALUES +(1, 2, 37, 16, 1, 3, '2026-01-07', '09:00:00', 'COMPLETED'), +(2, 8, 38, 17, 2, 8, '2026-01-09', '10:30:00', 'COMPLETED'), +(3, 4, 39, 18, 3, 13, '2026-01-11', '13:00:00', 'MISSED'), +(4, 8, 40, 19, 1, 6, '2026-01-13', '14:30:00', 'CANCELLED'), +(5, 5, 41, 20, 2, 7, '2026-01-15', '16:00:00', 'COMPLETED'), +(6, 8, 42, 21, 3, 12, '2026-01-17', '09:00:00', 'COMPLETED'), +(7, 2, 43, 22, 1, 5, '2026-01-19', '10:30:00', 'COMPLETED'), +(8, 8, 44, 23, 2, 10, '2026-01-21', '13:00:00', 'MISSED'), +(9, 4, 45, 24, 3, 11, '2026-01-23', '14:30:00', 'CANCELLED'), +(10, 8, 46, 25, 1, 4, '2026-01-25', '16:00:00', 'COMPLETED'), +(11, 6, 47, 26, 2, 9, '2026-01-27', '09:00:00', 'COMPLETED'), +(12, 7, 48, 27, 3, 14, '2026-01-29', '10:30:00', 'COMPLETED'), +(13, 2, 49, 28, 1, 3, '2026-01-31', '13:00:00', 'MISSED'), +(14, 4, 50, 29, 2, 8, '2026-02-02', '14:30:00', 'CANCELLED'), +(15, 5, 51, 30, 3, 13, '2026-02-04', '16:00:00', 'COMPLETED'), +(16, 1, 52, 31, 1, 6, '2026-02-06', '09:00:00', 'COMPLETED'), +(17, 2, 53, 32, 2, 7, '2026-02-08', '10:30:00', 'COMPLETED'), +(18, 3, 54, 33, 3, 12, '2026-02-10', '13:00:00', 'MISSED'), +(19, 4, 55, 34, 2, 9, '2026-02-12', '14:30:00', 'CANCELLED'), +(20, 5, 56, 35, 3, 14, '2026-02-14', '16:00:00', 'COMPLETED'), +(21, 1, 57, 36, 1, 3, '2026-02-16', '09:00:00', 'COMPLETED'), +(22, 4, 58, 37, 2, 8, '2026-02-18', '10:30:00', 'COMPLETED'), +(23, 4, 59, 38, 3, 13, '2026-02-20', '13:00:00', 'MISSED'), +(24, 5, 60, 39, 1, 6, '2026-02-22', '14:30:00', 'CANCELLED'), +(25, 2, 61, 40, 2, 7, '2026-02-24', '16:00:00', 'COMPLETED'), +(26, 1, 62, 41, 3, 12, '2026-02-26', '09:00:00', 'COMPLETED'), +(27, 2, 63, 42, 1, 5, '2026-02-28', '10:30:00', 'COMPLETED'), +(28, 3, 64, 43, 2, 10, '2026-03-02', '13:00:00', 'MISSED'), +(29, 2, 65, 44, 3, 11, '2026-03-04', '14:30:00', 'CANCELLED'), +(30, 8, 66, 45, 1, 4, '2026-03-06', '16:00:00', 'COMPLETED'), +(31, 2, 67, 46, 2, 9, '2026-03-08', '09:00:00', 'COMPLETED'), +(32, 5, 68, 47, 3, 14, '2026-03-10', '10:30:00', 'COMPLETED'), +(33, 3, 69, 48, 1, 3, '2026-03-12', '13:00:00', 'MISSED'), +(34, 4, 70, 49, 2, 8, '2026-03-14', '14:30:00', 'CANCELLED'), +(35, 5, 71, 50, 3, 13, '2026-03-16', '16:00:00', 'COMPLETED'), +(36, 7, 72, 51, 1, 6, '2026-03-18', '09:00:00', 'COMPLETED'), +(37, 4, 73, 52, 2, 7, '2026-03-20', '10:30:00', 'COMPLETED'), +(38, 4, 74, 53, 3, 12, '2026-03-22', '13:00:00', 'MISSED'), +(39, 4, 75, 54, 1, 5, '2026-03-24', '14:30:00', 'CANCELLED'), +(40, 5, 76, 55, 2, 10, '2026-03-26', '16:00:00', 'COMPLETED'), +(41, 1, 77, 56, 3, 11, '2026-03-28', '09:00:00', 'COMPLETED'), +(42, 2, 78, 57, 1, 4, '2026-03-30', '10:30:00', 'COMPLETED'), +(43, 6, 79, 58, 2, 9, '2026-04-01', '13:00:00', 'BOOKED'), +(44, 8, 80, 59, 3, 14, '2026-04-03', '14:30:00', 'BOOKED'), +(45, 5, 81, 60, 1, 3, '2026-04-05', '16:00:00', 'BOOKED'), +(46, 3, 82, 61, 2, 8, '2026-04-07', '09:00:00', 'BOOKED'), +(47, 2, 83, 62, 3, 13, '2026-04-09', '10:30:00', 'BOOKED'), +(48, 3, 84, 63, 1, 6, '2026-04-11', '13:00:00', 'BOOKED'), +(49, 4, 85, 64, 2, 7, '2026-04-13', '14:30:00', 'BOOKED'), +(50, 4, 86, 65, 3, 12, '2026-04-15', '16:00:00', 'BOOKED'), +(51, 4, 87, 66, 1, 5, '2026-04-17', '09:00:00', 'BOOKED'), +(52, 2, 88, 67, 2, 10, '2026-04-19', '10:30:00', 'BOOKED'), +(53, 2, 89, 68, 3, 11, '2026-04-21', '13:00:00', 'BOOKED'), +(54, 4, 90, 69, 1, 4, '2026-04-23', '14:30:00', 'BOOKED'), +(55, 5, 91, 70, 2, 9, '2026-04-25', '16:00:00', 'BOOKED'), +(56, 1, 92, 71, 3, 14, '2026-04-27', '09:00:00', 'BOOKED'), +(57, 2, 93, 72, 1, 3, '2026-04-29', '10:30:00', 'BOOKED'), +(58, 8, 94, 73, 2, 8, '2026-05-01', '13:00:00', 'BOOKED'), +(59, 4, 95, 74, 3, 13, '2026-05-03', '14:30:00', 'BOOKED'), +(60, 5, 96, 75, 1, 6, '2026-05-05', '16:00:00', 'BOOKED'), +(61, 1, 97, 76, 2, 7, '2026-01-07', '09:00:00', 'COMPLETED'), +(62, 2, 98, 77, 3, 12, '2026-01-09', '10:30:00', 'COMPLETED'), +(63, 3, 99, 78, 1, 5, '2026-01-11', '13:00:00', 'MISSED'), +(64, 7, 100, 79, 2, 10, '2026-01-13', '14:30:00', 'CANCELLED'), +(65, 2, 37, 16, 1, 3, '2026-01-15', '16:00:00', 'COMPLETED'), +(66, 8, 38, 17, 2, 8, '2026-01-17', '09:00:00', 'COMPLETED'), +(67, 4, 39, 18, 3, 13, '2026-01-19', '10:30:00', 'COMPLETED'), +(68, 8, 40, 19, 1, 6, '2026-01-21', '13:00:00', 'MISSED'), +(69, 4, 41, 20, 2, 7, '2026-01-23', '14:30:00', 'CANCELLED'), +(70, 8, 42, 21, 3, 12, '2026-01-25', '16:00:00', 'COMPLETED'), +(71, 1, 43, 22, 1, 5, '2026-01-27', '09:00:00', 'COMPLETED'), +(72, 8, 44, 23, 2, 10, '2026-01-29', '10:30:00', 'COMPLETED'), +(73, 4, 45, 24, 3, 11, '2026-01-31', '13:00:00', 'MISSED'), +(74, 8, 46, 25, 1, 4, '2026-02-02', '14:30:00', 'CANCELLED'), +(75, 6, 47, 26, 2, 9, '2026-02-04', '16:00:00', 'COMPLETED'), +(76, 7, 48, 27, 3, 14, '2026-02-06', '09:00:00', 'COMPLETED'), +(77, 2, 49, 28, 1, 3, '2026-02-08', '10:30:00', 'COMPLETED'), +(78, 4, 50, 29, 2, 8, '2026-02-10', '13:00:00', 'MISSED'), +(79, 4, 51, 30, 3, 13, '2026-02-12', '14:30:00', 'CANCELLED'), +(80, 5, 52, 31, 1, 6, '2026-02-14', '16:00:00', 'COMPLETED'), +(81, 1, 53, 32, 2, 7, '2026-02-16', '09:00:00', 'COMPLETED'), +(82, 2, 54, 33, 3, 12, '2026-02-18', '10:30:00', 'COMPLETED'), +(83, 3, 55, 34, 2, 9, '2026-02-20', '13:00:00', 'MISSED'), +(84, 4, 56, 35, 3, 14, '2026-02-22', '14:30:00', 'CANCELLED'), +(85, 5, 57, 36, 1, 3, '2026-02-24', '16:00:00', 'COMPLETED'), +(86, 4, 58, 37, 2, 8, '2026-02-26', '09:00:00', 'COMPLETED'), +(87, 4, 59, 38, 3, 13, '2026-02-28', '10:30:00', 'COMPLETED'), +(88, 2, 60, 39, 1, 6, '2026-03-02', '13:00:00', 'MISSED'), +(89, 2, 61, 40, 2, 7, '2026-03-04', '14:30:00', 'CANCELLED'), +(90, 5, 62, 41, 3, 12, '2026-03-06', '16:00:00', 'COMPLETED'); + +INSERT INTO adoption (adoptionId, petId, customerId, employeeId, sourceStoreId, adoptionDate, adoptionStatus) VALUES +(1, 37, 16, 3, 1, '2026-01-08', 'Completed'), +(2, 38, 17, 8, 2, '2026-01-13', 'Completed'), +(3, 39, 18, 13, 3, '2026-01-18', 'Completed'), +(4, 40, 19, 6, 1, '2026-01-23', 'Completed'), +(5, 41, 20, 7, 2, '2026-01-28', 'Completed'), +(6, 42, 21, 12, 3, '2026-02-02', 'Completed'), +(7, 43, 22, 5, 1, '2026-02-07', 'Completed'), +(8, 44, 23, 10, 2, '2026-02-12', 'Completed'), +(9, 45, 24, 11, 3, '2026-02-17', 'Completed'), +(10, 46, 25, 4, 1, '2026-02-22', 'Completed'), +(11, 47, 26, 9, 2, '2026-02-27', 'Completed'), +(12, 48, 27, 14, 3, '2026-03-04', 'Completed'), +(13, 49, 28, 3, 1, '2026-03-09', 'Completed'), +(14, 50, 29, 8, 2, '2026-03-14', 'Completed'), +(15, 51, 30, 13, 3, '2026-03-19', 'Completed'), +(16, 52, 31, 6, 1, '2026-03-24', 'Completed'), +(17, 53, 32, 7, 2, '2026-03-29', 'Completed'), +(18, 54, 33, 12, 3, '2026-04-03', 'Completed'); + +INSERT INTO cart (cartId, userId, storeId, couponId, cartStatus, subtotalAmount, discountAmount, totalAmount) VALUES +(1, 3, 1, 1, 'CHECKED_OUT', 44.51, 0.00, 44.51), +(2, 4, 1, 2, 'CHECKED_OUT', 252.00, 25.20, 226.80), +(3, 5, 1, 3, 'CHECKED_OUT', 432.33, 5.00, 427.33), +(4, 6, 1, 7, 'CHECKED_OUT', 108.67, 13.04, 95.63), +(5, 7, 2, 1, 'CHECKED_OUT', 512.62, 0.00, 512.62), +(6, 8, 2, 1, 'CHECKED_OUT', 481.83, 0.00, 481.83), +(7, 9, 2, 4, 'CHECKED_OUT', 78.06, 11.71, 66.35), +(8, 10, 2, 5, 'CHECKED_OUT', 212.94, 8.00, 204.94), +(9, 11, 3, 1, 'CHECKED_OUT', 729.31, 0.00, 729.31), +(10, 12, 3, 6, 'CHECKED_OUT', 221.87, 22.19, 199.68), +(11, 13, 3, 1, 'CHECKED_OUT', 162.19, 0.00, 162.19), +(12, 14, 3, 2, 'CHECKED_OUT', 593.40, 59.34, 534.06), +(13, 15, 1, 3, 'ACTIVE', 78.65, 5.00, 73.65), +(14, 16, 2, 7, 'ACTIVE', 140.29, 16.83, 123.46), +(15, 17, 3, 1, 'ACTIVE', 261.60, 0.00, 261.60), +(16, 18, 1, 1, 'ACTIVE', 93.84, 0.00, 93.84), +(17, 19, 2, 4, 'ACTIVE', 167.87, 25.18, 142.69), +(18, 20, 3, 5, 'ACTIVE', 206.96, 8.00, 198.96), +(19, 21, 1, 1, 'ACTIVE', 257.56, 0.00, 257.56), +(20, 22, 2, 6, 'ACTIVE', 258.59, 25.86, 232.73), +(21, 23, 3, 1, 'ACTIVE', 340.69, 0.00, 340.69), +(22, 24, 1, 2, 'ACTIVE', 152.52, 15.25, 137.27), +(23, 25, 2, 3, 'ACTIVE', 266.50, 5.00, 261.50), +(24, 26, 3, 7, 'ACTIVE', 231.01, 27.72, 203.29), +(25, 27, 1, 1, 'ACTIVE', 393.87, 0.00, 393.87), +(26, 28, 2, 1, 'ACTIVE', 437.53, 0.00, 437.53), +(27, 29, 3, 4, 'ACTIVE', 258.63, 38.79, 219.84), +(28, 30, 1, 5, 'ACTIVE', 132.17, 8.00, 124.17), +(29, 31, 2, 1, 'ACTIVE', 250.10, 0.00, 250.10), +(30, 32, 3, 6, 'ACTIVE', 498.48, 49.85, 448.63), +(31, 33, 1, 1, 'ABANDONED', 37.99, 0.00, 37.99), +(32, 34, 2, 2, 'ABANDONED', 318.98, 31.90, 287.08), +(33, 35, 3, 3, 'ABANDONED', 399.18, 5.00, 394.18), +(34, 36, 1, 7, 'ABANDONED', 79.36, 9.52, 69.84), +(35, 37, 2, 1, 'ABANDONED', 446.97, 0.00, 446.97), +(36, 38, 3, 1, 'ABANDONED', 714.69, 0.00, 714.69), +(37, 39, 1, 4, 'ABANDONED', 58.94, 0.00, 58.94), +(38, 40, 2, 5, 'ABANDONED', 248.85, 8.00, 240.85), +(39, 41, 3, 1, 'ABANDONED', 444.14, 0.00, 444.14), +(40, 42, 1, 6, 'ABANDONED', 178.85, 17.89, 160.96); + +INSERT INTO cart_item (cartItemId, cartId, prodId, quantity, unitPrice) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 2, 9.71), +(3, 2, 8, 2, 63.05), +(4, 2, 19, 3, 25.27), +(5, 2, 30, 1, 50.09), +(6, 3, 15, 3, 16.38), +(7, 3, 26, 1, 32.67), +(8, 3, 37, 2, 97.56), +(9, 3, 48, 3, 51.80), +(10, 4, 22, 1, 15.25), +(11, 4, 33, 2, 46.71), +(12, 5, 29, 2, 45.73), +(13, 5, 40, 3, 135.69), +(14, 5, 51, 1, 14.09), +(15, 6, 36, 3, 84.85), +(16, 6, 47, 1, 45.97), +(17, 6, 58, 2, 45.82), +(18, 6, 69, 3, 29.89), +(19, 7, 43, 1, 22.68), +(20, 7, 54, 2, 27.69), +(21, 8, 50, 2, 63.44), +(22, 8, 61, 3, 10.69), +(23, 8, 72, 1, 53.99), +(24, 9, 57, 3, 41.29), +(25, 9, 68, 1, 27.49), +(26, 9, 79, 2, 165.99), +(27, 9, 90, 3, 81.99), +(28, 10, 64, 1, 17.89), +(29, 10, 75, 2, 101.99), +(30, 11, 71, 2, 37.99), +(31, 11, 82, 3, 25.10), +(32, 11, 93, 1, 10.91), +(33, 12, 78, 3, 149.99), +(34, 12, 89, 1, 74.88), +(35, 12, 100, 2, 23.04), +(36, 12, 11, 3, 7.49), +(37, 13, 85, 1, 46.43), +(38, 13, 96, 2, 16.11), +(39, 14, 92, 2, 9.17), +(40, 14, 3, 3, 35.93), +(41, 14, 14, 1, 14.16), +(42, 15, 99, 3, 21.31), +(43, 15, 10, 1, 73.89), +(44, 15, 21, 2, 10.89), +(45, 15, 32, 3, 34.00), +(46, 16, 6, 1, 52.20), +(47, 16, 17, 2, 20.82), +(48, 17, 13, 2, 11.93), +(49, 17, 24, 3, 23.96), +(50, 17, 35, 1, 72.13), +(51, 18, 20, 3, 27.49), +(52, 18, 31, 1, 21.29), +(53, 18, 42, 2, 16.86), +(54, 18, 53, 3, 23.16), +(55, 19, 27, 1, 37.02), +(56, 19, 38, 2, 110.27), +(57, 20, 34, 2, 59.42), +(58, 20, 45, 3, 34.33), +(59, 20, 56, 1, 36.76), +(60, 21, 41, 3, 11.04), +(61, 21, 52, 1, 18.62), +(62, 21, 63, 2, 15.49), +(63, 21, 74, 3, 85.99), +(64, 22, 48, 1, 51.80), +(65, 22, 59, 2, 50.36), +(66, 23, 55, 2, 32.22), +(67, 23, 66, 3, 22.69), +(68, 23, 77, 1, 133.99), +(69, 24, 62, 3, 13.09), +(70, 24, 73, 1, 69.99), +(71, 24, 84, 2, 39.32), +(72, 24, 95, 3, 14.37), +(73, 25, 69, 1, 29.89), +(74, 25, 80, 2, 181.99), +(75, 26, 76, 2, 117.99), +(76, 26, 87, 3, 60.66), +(77, 26, 98, 1, 19.57), +(78, 27, 83, 3, 32.21), +(79, 27, 94, 1, 12.64), +(80, 27, 5, 2, 46.78), +(81, 27, 16, 3, 18.60), +(82, 28, 90, 1, 81.99), +(83, 28, 1, 2, 25.09), +(84, 29, 97, 2, 17.84), +(85, 29, 8, 3, 63.05), +(86, 29, 19, 1, 25.27), +(87, 30, 4, 3, 41.36), +(88, 30, 15, 1, 16.38), +(89, 30, 26, 2, 32.67), +(90, 30, 37, 3, 97.56), +(91, 31, 11, 1, 7.49), +(92, 31, 22, 2, 15.25), +(93, 32, 18, 2, 23.05), +(94, 32, 29, 3, 45.73), +(95, 32, 40, 1, 135.69), +(96, 33, 25, 3, 28.31), +(97, 33, 36, 1, 84.85), +(98, 33, 47, 2, 45.97), +(99, 33, 58, 3, 45.82), +(100, 34, 32, 1, 34.00), +(101, 34, 43, 2, 22.68), +(102, 35, 39, 2, 122.98), +(103, 35, 50, 3, 63.44), +(104, 35, 61, 1, 10.69), +(105, 36, 46, 3, 40.15), +(106, 36, 57, 1, 41.29), +(107, 36, 68, 2, 27.49), +(108, 36, 79, 3, 165.99), +(109, 37, 53, 1, 23.16), +(110, 37, 64, 2, 17.89), +(111, 38, 60, 2, 54.89), +(112, 38, 71, 3, 37.99), +(113, 38, 82, 1, 25.10), +(114, 39, 67, 3, 25.09), +(115, 39, 78, 1, 149.99), +(116, 39, 89, 2, 74.88), +(117, 39, 100, 3, 23.04), +(118, 40, 74, 1, 85.99), +(119, 40, 85, 2, 46.43); + +INSERT INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES +(1, '2026-02-02 10:11:00', 37.83, 'Cash', 3, 1, 3, 0, NULL, 'ONLINE', 1, 1, 44.51, 0.00, 6.68, 0), +(2, '2026-02-03 10:22:00', 192.78, 'Card', 4, 1, 4, 0, NULL, 'ONLINE', 2, 2, 252.00, 25.20, 34.02, 0), +(3, '2026-02-04 10:33:00', 363.23, 'Card', 5, 1, 5, 0, NULL, 'ONLINE', 3, 3, 432.33, 5.00, 64.10, 0), +(4, '2026-02-05 10:44:00', 81.29, 'Cash', 6, 1, 6, 0, NULL, 'ONLINE', 4, 7, 108.67, 13.04, 14.34, 0), +(5, '2026-02-06 10:55:00', 435.73, 'Card', 7, 2, 7, 0, NULL, 'ONLINE', 5, 1, 512.62, 0.00, 76.89, 0), +(6, '2026-02-07 11:06:00', 409.56, 'Card', 8, 2, 8, 0, NULL, 'ONLINE', 6, 1, 481.83, 0.00, 72.27, 0), +(7, '2026-02-08 11:17:00', 56.40, 'Cash', 9, 2, 9, 0, NULL, 'ONLINE', 7, 4, 78.06, 11.71, 9.95, 0), +(8, '2026-02-09 11:28:00', 174.20, 'Card', 10, 2, 10, 0, NULL, 'ONLINE', 8, 5, 212.94, 8.00, 30.74, 0), +(9, '2026-02-10 11:39:00', 619.91, 'Card', 11, 3, 11, 0, NULL, 'ONLINE', 9, 1, 729.31, 0.00, 109.40, 0), +(10, '2026-02-11 11:50:00', 169.73, 'Card', 12, 3, 12, 0, NULL, 'ONLINE', 10, 6, 221.87, 22.19, 29.95, 0), +(11, '2026-02-12 12:01:00', 137.86, 'Cash', 13, 3, 13, 0, NULL, 'ONLINE', 11, 1, 162.19, 0.00, 24.33, 0), +(12, '2026-02-13 12:12:00', 453.95, 'Card', 14, 3, 14, 0, NULL, 'ONLINE', 12, 2, 593.40, 59.34, 80.11, 0), +(13, '2026-01-05 09:15:00', 82.72, 'Card', 3, 1, 15, 0, NULL, 'IN_STORE', NULL, 1, 82.72, 0.00, 0.00, 0), +(14, '2026-01-05 09:52:00', 120.43, 'Card', 8, 2, 16, 0, NULL, 'IN_STORE', NULL, 2, 133.81, 13.38, 0.00, 12), +(15, '2026-01-06 10:29:00', 153.21, 'Cash', 13, 3, 17, 0, NULL, 'IN_STORE', NULL, 1, 153.21, 0.00, 0.00, 15), +(16, '2026-01-06 11:06:00', 20.27, 'Card', 6, 1, 18, 0, NULL, 'IN_STORE', NULL, 3, 25.27, 5.00, 0.00, 2), +(17, '2026-01-07 11:43:00', 58.96, 'Cash', 7, 2, 19, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), +(18, '2026-01-07 12:20:00', 124.54, 'Card', 12, 3, 20, 0, NULL, 'IN_STORE', NULL, 7, 141.52, 16.98, 0.00, 12), +(19, '2026-01-08 12:57:00', 118.84, 'Card', 5, 1, 21, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), +(20, '2026-01-08 13:34:00', 167.02, 'Cash', 10, 2, 22, 0, NULL, 'IN_STORE', NULL, 4, 196.50, 29.48, 0.00, 16), +(21, '2026-01-09 14:11:00', 367.69, 'Card', 11, 3, 23, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), +(22, '2026-01-09 14:48:00', 57.62, 'Cash', 4, 1, 24, 0, NULL, 'IN_STORE', NULL, 1, 57.62, 0.00, 0.00, 5), +(23, '2026-01-10 15:25:00', 84.03, 'Card', 9, 2, 25, 0, NULL, 'IN_STORE', NULL, 6, 93.37, 9.34, 0.00, 8), +(24, '2026-01-10 16:02:00', 297.25, 'Card', 14, 3, 26, 0, NULL, 'IN_STORE', NULL, 1, 297.25, 0.00, 0.00, 29), +(25, '2026-01-11 16:39:00', 35.78, 'Cash', 3, 1, 27, 0, NULL, 'IN_STORE', NULL, 2, 35.78, 0.00, 0.00, 3), +(26, '2026-01-11 17:16:00', 136.99, 'Card', 8, 2, 28, 0, NULL, 'IN_STORE', NULL, 1, 136.99, 0.00, 0.00, 13), +(27, '2026-01-12 17:53:00', 300.52, 'Cash', 13, 3, 29, 0, NULL, 'IN_STORE', NULL, 3, 305.52, 5.00, 0.00, 30), +(28, '2026-01-12 18:30:00', 165.99, 'Card', 6, 1, 30, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), +(29, '2026-01-13 19:07:00', 91.28, 'Card', 7, 2, 31, 0, NULL, 'IN_STORE', NULL, 7, 103.73, 12.45, 0.00, 9), +(30, '2026-01-13 19:44:00', 198.88, 'Cash', 12, 3, 32, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), +(31, '2026-01-14 20:21:00', 25.28, 'Card', 5, 1, 33, 0, NULL, 'IN_STORE', NULL, 4, 25.28, 0.00, 0.00, 2), +(32, '2026-01-14 20:58:00', 58.51, 'Cash', 10, 2, 34, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), +(33, '2026-01-15 21:35:00', 314.15, 'Card', 11, 3, 35, 0, NULL, 'IN_STORE', NULL, 1, 314.15, 0.00, 0.00, 31), +(34, '2026-01-15 22:12:00', 61.62, 'Card', 4, 1, 36, 0, NULL, 'IN_STORE', NULL, 6, 68.47, 6.85, 0.00, 6), +(35, '2026-01-16 22:49:00', 49.61, 'Cash', 9, 2, 37, 0, NULL, 'IN_STORE', NULL, 1, 49.61, 0.00, 0.00, 4), +(36, '2026-01-16 23:26:00', 196.32, 'Card', 14, 3, 38, 0, NULL, 'IN_STORE', NULL, 2, 218.13, 21.81, 0.00, 19), +(37, '2026-01-18 00:03:00', 47.92, 'Cash', 3, 1, 39, 0, NULL, 'IN_STORE', NULL, 1, 47.92, 0.00, 0.00, 4), +(38, '2026-01-18 00:40:00', 121.03, 'Card', 8, 2, 40, 0, NULL, 'IN_STORE', NULL, 3, 126.03, 5.00, 0.00, 12), +(39, '2026-01-19 01:17:00', 187.91, 'Card', 13, 3, 41, 0, NULL, 'IN_STORE', NULL, 1, 187.91, 0.00, 0.00, 18), +(40, '2026-01-19 01:54:00', 108.22, 'Cash', 6, 1, 42, 0, NULL, 'IN_STORE', NULL, 7, 122.98, 14.76, 0.00, 10), +(41, '2026-01-20 02:31:00', 67.71, 'Card', 7, 2, 43, 0, NULL, 'IN_STORE', NULL, 1, 67.71, 0.00, 0.00, 6), +(42, '2026-01-20 03:08:00', 114.93, 'Cash', 12, 3, 44, 0, NULL, 'IN_STORE', NULL, 4, 135.21, 20.28, 0.00, 11), +(43, '2026-01-21 03:45:00', 55.38, 'Card', 5, 1, 45, 0, NULL, 'IN_STORE', NULL, 1, 55.38, 0.00, 0.00, 5), +(44, '2026-01-21 04:22:00', 286.34, 'Card', 10, 2, 46, 0, NULL, 'IN_STORE', NULL, 1, 286.34, 0.00, 0.00, 28), +(45, '2026-01-22 04:59:00', 83.62, 'Cash', 11, 3, 47, 0, NULL, 'IN_STORE', NULL, 6, 92.91, 9.29, 0.00, 8), +(46, '2026-01-22 05:36:00', 29.89, 'Card', 4, 1, 48, 0, NULL, 'IN_STORE', NULL, 1, 29.89, 0.00, 0.00, 2), +(47, '2026-01-23 06:13:00', 161.48, 'Cash', 9, 2, 49, 0, NULL, 'IN_STORE', NULL, 2, 179.42, 17.94, 0.00, 16), +(48, '2026-01-23 06:50:00', 210.14, 'Card', 14, 3, 50, 0, NULL, 'IN_STORE', NULL, 1, 210.14, 0.00, 0.00, 21), +(49, '2026-01-24 07:27:00', 73.64, 'Card', 3, 1, 51, 0, NULL, 'IN_STORE', NULL, 3, 78.64, 5.00, 0.00, 7), +(50, '2026-01-24 08:04:00', 179.28, 'Cash', 8, 2, 52, 0, NULL, 'IN_STORE', NULL, 1, 179.28, 0.00, 0.00, 17), +(51, '2026-01-25 08:41:00', 101.67, 'Card', 13, 3, 53, 0, NULL, 'IN_STORE', NULL, 7, 115.53, 13.86, 0.00, 10), +(52, '2026-01-25 09:18:00', 21.31, 'Cash', 6, 1, 54, 0, NULL, 'IN_STORE', NULL, 1, 21.31, 0.00, 0.00, 2), +(53, '2026-01-26 09:55:00', 79.57, 'Card', 7, 2, 55, 0, NULL, 'IN_STORE', NULL, 4, 93.61, 14.04, 0.00, 7), +(54, '2026-01-26 10:32:00', 156.49, 'Card', 12, 3, 56, 0, NULL, 'IN_STORE', NULL, 1, 156.49, 0.00, 0.00, 15), +(55, '2026-01-27 11:09:00', 28.32, 'Cash', 5, 1, 57, 0, NULL, 'IN_STORE', NULL, 1, 28.32, 0.00, 0.00, 2), +(56, '2026-01-27 11:46:00', 175.47, 'Card', 10, 2, 58, 0, NULL, 'IN_STORE', NULL, 6, 194.97, 19.50, 0.00, 17), +(57, '2026-01-28 12:23:00', 150.60, 'Cash', 11, 3, 59, 0, NULL, 'IN_STORE', NULL, 1, 150.60, 0.00, 0.00, 15), +(58, '2026-01-28 13:00:00', 45.73, 'Card', 4, 1, 60, 0, NULL, 'IN_STORE', NULL, 2, 45.73, 0.00, 0.00, 4), +(59, '2026-01-29 13:37:00', 132.93, 'Card', 9, 2, 61, 0, NULL, 'IN_STORE', NULL, 1, 132.93, 0.00, 0.00, 13), +(60, '2026-01-29 14:14:00', 261.49, 'Cash', 14, 3, 62, 0, NULL, 'IN_STORE', NULL, 3, 266.49, 5.00, 0.00, 26), +(61, '2026-01-30 14:51:00', 57.02, 'Card', 3, 1, 63, 0, NULL, 'IN_STORE', NULL, 1, 57.02, 0.00, 0.00, 5), +(62, '2026-01-30 15:28:00', 90.64, 'Cash', 8, 2, 64, 0, NULL, 'IN_STORE', NULL, 7, 103.00, 12.36, 0.00, 9), +(63, '2026-01-31 16:05:00', 228.91, 'Card', 13, 3, 65, 0, NULL, 'IN_STORE', NULL, 1, 228.91, 0.00, 0.00, 22), +(64, '2026-01-31 16:42:00', 50.36, 'Card', 6, 1, 66, 0, NULL, 'IN_STORE', NULL, 4, 50.36, 0.00, 0.00, 5), +(65, '2026-02-01 17:19:00', 53.77, 'Cash', 7, 2, 67, 0, NULL, 'IN_STORE', NULL, 1, 53.77, 0.00, 0.00, 5), +(66, '2026-02-01 17:56:00', 172.92, 'Card', 12, 3, 68, 0, NULL, 'IN_STORE', NULL, 1, 172.92, 0.00, 0.00, 17), +(67, '2026-02-02 18:33:00', 154.78, 'Cash', 5, 1, 69, 0, NULL, 'IN_STORE', NULL, 6, 171.98, 17.20, 0.00, 15), +(68, '2026-02-02 19:10:00', 198.21, 'Card', 10, 2, 70, 0, NULL, 'IN_STORE', NULL, 1, 198.21, 0.00, 0.00, 19), +(69, '2026-02-03 19:47:00', 134.85, 'Card', 11, 3, 71, 0, NULL, 'IN_STORE', NULL, 2, 149.83, 14.98, 0.00, 13), +(70, '2026-02-03 20:24:00', 74.88, 'Cash', 4, 1, 72, 0, NULL, 'IN_STORE', NULL, 1, 74.88, 0.00, 0.00, 7), +(71, '2026-02-04 21:01:00', 27.77, 'Card', 9, 2, 73, 0, NULL, 'IN_STORE', NULL, 3, 32.77, 5.00, 0.00, 2), +(72, '2026-02-04 21:38:00', 105.22, 'Cash', 14, 3, 74, 0, NULL, 'IN_STORE', NULL, 1, 105.22, 0.00, 0.00, 10), +(73, '2026-02-05 22:15:00', 72.79, 'Card', 3, 1, 75, 0, NULL, 'IN_STORE', NULL, 7, 82.72, 9.93, 0.00, 7), +(74, '2026-02-05 22:52:00', 133.81, 'Card', 8, 2, 76, 0, NULL, 'IN_STORE', NULL, 1, 133.81, 0.00, 0.00, 13), +(75, '2026-02-06 23:29:00', 130.23, 'Cash', 13, 3, 77, 0, NULL, 'IN_STORE', NULL, 4, 153.21, 22.98, 0.00, 13), +(76, '2026-02-07 00:06:00', 25.27, 'Card', 6, 1, 78, 0, NULL, 'IN_STORE', NULL, 1, 25.27, 0.00, 0.00, 2), +(77, '2026-02-08 00:43:00', 58.96, 'Cash', 7, 2, 79, 0, NULL, 'IN_STORE', NULL, 1, 58.96, 0.00, 0.00, 5), +(78, '2026-02-08 01:20:00', 127.37, 'Card', 12, 3, 80, 0, NULL, 'IN_STORE', NULL, 6, 141.52, 14.15, 0.00, 12), +(79, '2026-02-09 01:57:00', 118.84, 'Card', 5, 1, 81, 0, NULL, 'IN_STORE', NULL, 1, 118.84, 0.00, 0.00, 11), +(80, '2026-02-09 02:34:00', 176.85, 'Cash', 10, 2, 82, 0, NULL, 'IN_STORE', NULL, 2, 196.50, 19.65, 0.00, 17), +(81, '2026-02-10 03:11:00', 367.69, 'Card', 11, 3, 83, 0, NULL, 'IN_STORE', NULL, 1, 367.69, 0.00, 0.00, 36), +(82, '2026-02-10 03:48:00', 52.62, 'Cash', 4, 1, 84, 0, NULL, 'IN_STORE', NULL, 3, 57.62, 5.00, 0.00, 5), +(83, '2026-02-11 04:25:00', 93.37, 'Card', 9, 2, 85, 0, NULL, 'IN_STORE', NULL, 1, 93.37, 0.00, 0.00, 9), +(84, '2026-02-11 05:02:00', 261.58, 'Card', 14, 3, 86, 0, NULL, 'IN_STORE', NULL, 7, 297.25, 35.67, 0.00, 26), +(85, '2026-02-12 05:39:00', 35.78, 'Cash', 3, 1, 87, 0, NULL, 'IN_STORE', NULL, 1, 35.78, 0.00, 0.00, 3), +(86, '2026-02-12 06:16:00', 116.44, 'Card', 8, 2, 88, 0, NULL, 'IN_STORE', NULL, 4, 136.99, 20.55, 0.00, 11), +(87, '2026-02-13 06:53:00', 305.52, 'Cash', 13, 3, 89, 0, NULL, 'IN_STORE', NULL, 1, 305.52, 0.00, 0.00, 30), +(88, '2026-02-13 07:30:00', 165.99, 'Card', 6, 1, 90, 0, NULL, 'IN_STORE', NULL, 1, 165.99, 0.00, 0.00, 16), +(89, '2026-02-14 08:07:00', 93.36, 'Card', 7, 2, 91, 0, NULL, 'IN_STORE', NULL, 6, 103.73, 10.37, 0.00, 9), +(90, '2026-02-14 08:44:00', 198.88, 'Cash', 12, 3, 92, 0, NULL, 'IN_STORE', NULL, 1, 198.88, 0.00, 0.00, 19), +(91, '2026-02-15 09:21:00', 25.28, 'Card', 5, 1, 93, 0, NULL, 'IN_STORE', NULL, 2, 25.28, 0.00, 0.00, 2), +(92, '2026-02-15 09:58:00', 58.51, 'Cash', 10, 2, 94, 0, NULL, 'IN_STORE', NULL, 1, 58.51, 0.00, 0.00, 5), +(93, '2026-02-16 10:35:00', 309.15, 'Card', 11, 3, 95, 0, NULL, 'IN_STORE', NULL, 3, 314.15, 5.00, 0.00, 30), +(94, '2026-02-16 11:12:00', 68.47, 'Card', 4, 1, 96, 0, NULL, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 6), +(95, '2026-02-17 11:49:00', 49.61, 'Cash', 9, 2, 97, 0, NULL, 'IN_STORE', NULL, 7, 49.61, 0.00, 0.00, 4), +(96, '2026-02-13 10:11:00', 34.80, 'Card', 3, 1, 3, 1, 1, 'IN_STORE', NULL, 1, 34.80, 0.00, 0.00, 0), +(97, '2026-02-15 10:22:00', 88.32, 'Card', 4, 1, 4, 1, 2, 'IN_STORE', NULL, 1, 88.32, 0.00, 0.00, 0), +(98, '2026-02-17 10:33:00', 49.05, 'Card', 5, 1, 5, 1, 3, 'IN_STORE', NULL, 1, 49.05, 0.00, 0.00, 0), +(99, '2026-02-19 10:44:00', 15.25, 'Card', 6, 1, 6, 1, 4, 'IN_STORE', NULL, 1, 15.25, 0.00, 0.00, 0), +(100, '2026-02-21 10:55:00', 181.42, 'Card', 7, 2, 7, 1, 5, 'IN_STORE', NULL, 1, 181.42, 0.00, 0.00, 0), +(101, '2026-02-23 11:06:00', 130.82, 'Card', 8, 2, 8, 1, 6, 'IN_STORE', NULL, 1, 130.82, 0.00, 0.00, 0), +(102, '2026-02-25 11:17:00', 50.37, 'Card', 9, 2, 9, 1, 7, 'IN_STORE', NULL, 1, 50.37, 0.00, 0.00, 0), +(103, '2026-02-27 11:28:00', 74.13, 'Card', 10, 2, 10, 1, 8, 'IN_STORE', NULL, 1, 74.13, 0.00, 0.00, 0), +(104, '2026-03-01 11:39:00', 68.78, 'Card', 11, 3, 11, 1, 9, 'IN_STORE', NULL, 1, 68.78, 0.00, 0.00, 0), +(105, '2026-03-03 11:50:00', 17.89, 'Card', 12, 3, 12, 1, 10, 'IN_STORE', NULL, 1, 17.89, 0.00, 0.00, 0), +(106, '2026-03-05 12:01:00', 63.09, 'Card', 13, 3, 13, 1, 11, 'IN_STORE', NULL, 1, 63.09, 0.00, 0.00, 0), +(107, '2026-03-07 12:12:00', 149.99, 'Card', 14, 3, 14, 1, 12, 'IN_STORE', NULL, 1, 149.99, 0.00, 0.00, 0), +(108, '2026-01-28 09:15:00', 41.36, 'Card', 3, 1, 15, 1, 13, 'IN_STORE', NULL, 1, 41.36, 0.00, 0.00, 0), +(109, '2026-01-29 09:52:00', 68.47, 'Card', 8, 2, 16, 1, 14, 'IN_STORE', NULL, 1, 68.47, 0.00, 0.00, 0), +(110, '2026-01-31 10:29:00', 14.16, 'Card', 13, 3, 17, 1, 15, 'IN_STORE', NULL, 1, 14.16, 0.00, 0.00, 0); + +INSERT INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 2, 9.71), +(3, 2, 8, 2, 63.05), +(4, 2, 19, 3, 25.27), +(5, 2, 30, 1, 50.09), +(6, 3, 15, 3, 16.38), +(7, 3, 26, 1, 32.67), +(8, 3, 37, 2, 97.56), +(9, 3, 48, 3, 51.80), +(10, 4, 22, 1, 15.25), +(11, 4, 33, 2, 46.71), +(12, 5, 29, 2, 45.73), +(13, 5, 40, 3, 135.69), +(14, 5, 51, 1, 14.09), +(15, 6, 36, 3, 84.85), +(16, 6, 47, 1, 45.97), +(17, 6, 58, 2, 45.82), +(18, 6, 69, 3, 29.89), +(19, 7, 43, 1, 22.68), +(20, 7, 54, 2, 27.69), +(21, 8, 50, 2, 63.44), +(22, 8, 61, 3, 10.69), +(23, 8, 72, 1, 53.99), +(24, 9, 57, 3, 41.29), +(25, 9, 68, 1, 27.49), +(26, 9, 79, 2, 165.99), +(27, 9, 90, 3, 81.99), +(28, 10, 64, 1, 17.89), +(29, 10, 75, 2, 101.99), +(30, 11, 71, 2, 37.99), +(31, 11, 82, 3, 25.10), +(32, 11, 93, 1, 10.91), +(33, 12, 78, 3, 149.99), +(34, 12, 89, 1, 74.88), +(35, 12, 100, 2, 23.04), +(36, 12, 11, 3, 7.49), +(37, 13, 4, 2, 41.36), +(38, 14, 9, 1, 68.47), +(39, 14, 26, 2, 32.67), +(40, 15, 14, 2, 14.16), +(41, 15, 31, 1, 21.29), +(42, 15, 48, 2, 51.80), +(43, 16, 19, 1, 25.27), +(44, 17, 24, 2, 23.96), +(45, 17, 41, 1, 11.04), +(46, 18, 29, 1, 45.73), +(47, 18, 46, 2, 40.15), +(48, 18, 63, 1, 15.49), +(49, 19, 34, 2, 59.42), +(50, 20, 39, 1, 122.98), +(51, 20, 56, 2, 36.76), +(52, 21, 44, 2, 28.51), +(53, 21, 61, 1, 10.69), +(54, 21, 78, 2, 149.99), +(55, 22, 49, 1, 57.62), +(56, 23, 54, 2, 27.69), +(57, 23, 71, 1, 37.99), +(58, 24, 59, 1, 50.36), +(59, 24, 76, 2, 117.99), +(60, 24, 93, 1, 10.91), +(61, 25, 64, 2, 17.89), +(62, 26, 69, 1, 29.89), +(63, 26, 86, 2, 53.55), +(64, 27, 74, 2, 85.99), +(65, 27, 91, 1, 7.44), +(66, 27, 8, 2, 63.05), +(67, 28, 79, 1, 165.99), +(68, 29, 84, 2, 39.32), +(69, 29, 1, 1, 25.09), +(70, 30, 89, 1, 74.88), +(71, 30, 6, 2, 52.20), +(72, 30, 23, 1, 19.60), +(73, 31, 94, 2, 12.64), +(74, 32, 99, 1, 21.31), +(75, 32, 16, 2, 18.60), +(76, 33, 4, 2, 41.36), +(77, 33, 21, 1, 10.89), +(78, 33, 38, 2, 110.27), +(79, 34, 9, 1, 68.47), +(80, 35, 14, 2, 14.16), +(81, 35, 31, 1, 21.29), +(82, 36, 19, 1, 25.27), +(83, 36, 36, 2, 84.85), +(84, 36, 53, 1, 23.16), +(85, 37, 24, 2, 23.96), +(86, 38, 29, 1, 45.73), +(87, 38, 46, 2, 40.15), +(88, 39, 34, 2, 59.42), +(89, 39, 51, 1, 14.09), +(90, 39, 68, 2, 27.49), +(91, 40, 39, 1, 122.98), +(92, 41, 44, 2, 28.51), +(93, 41, 61, 1, 10.69), +(94, 42, 49, 1, 57.62), +(95, 42, 66, 2, 22.69), +(96, 42, 83, 1, 32.21), +(97, 43, 54, 2, 27.69), +(98, 44, 59, 1, 50.36), +(99, 44, 76, 2, 117.99), +(100, 45, 64, 2, 17.89), +(101, 45, 81, 1, 17.99), +(102, 45, 98, 2, 19.57), +(103, 46, 69, 1, 29.89), +(104, 47, 74, 2, 85.99), +(105, 47, 91, 1, 7.44), +(106, 48, 79, 1, 165.99), +(107, 48, 96, 2, 16.11), +(108, 48, 13, 1, 11.93), +(109, 49, 84, 2, 39.32), +(110, 50, 89, 1, 74.88), +(111, 50, 6, 2, 52.20), +(112, 51, 94, 2, 12.64), +(113, 51, 11, 1, 7.49), +(114, 51, 28, 2, 41.38), +(115, 52, 99, 1, 21.31), +(116, 53, 4, 2, 41.36), +(117, 53, 21, 1, 10.89), +(118, 54, 9, 1, 68.47), +(119, 54, 26, 2, 32.67), +(120, 54, 43, 1, 22.68), +(121, 55, 14, 2, 14.16), +(122, 56, 19, 1, 25.27), +(123, 56, 36, 2, 84.85), +(124, 57, 24, 2, 23.96), +(125, 57, 41, 1, 11.04), +(126, 57, 58, 2, 45.82), +(127, 58, 29, 1, 45.73), +(128, 59, 34, 2, 59.42), +(129, 59, 51, 1, 14.09), +(130, 60, 39, 1, 122.98), +(131, 60, 56, 2, 36.76), +(132, 60, 73, 1, 69.99), +(133, 61, 44, 2, 28.51), +(134, 62, 49, 1, 57.62), +(135, 62, 66, 2, 22.69), +(136, 63, 54, 2, 27.69), +(137, 63, 71, 1, 37.99), +(138, 63, 88, 2, 67.77), +(139, 64, 59, 1, 50.36), +(140, 65, 64, 2, 17.89), +(141, 65, 81, 1, 17.99), +(142, 66, 69, 1, 29.89), +(143, 66, 86, 2, 53.55), +(144, 66, 3, 1, 35.93), +(145, 67, 74, 2, 85.99), +(146, 68, 79, 1, 165.99), +(147, 68, 96, 2, 16.11), +(148, 69, 84, 2, 39.32), +(149, 69, 1, 1, 25.09), +(150, 69, 18, 2, 23.05), +(151, 70, 89, 1, 74.88), +(152, 71, 94, 2, 12.64), +(153, 71, 11, 1, 7.49), +(154, 72, 99, 1, 21.31), +(155, 72, 16, 2, 18.60), +(156, 72, 33, 1, 46.71), +(157, 73, 4, 2, 41.36), +(158, 74, 9, 1, 68.47), +(159, 74, 26, 2, 32.67), +(160, 75, 14, 2, 14.16), +(161, 75, 31, 1, 21.29), +(162, 75, 48, 2, 51.80), +(163, 76, 19, 1, 25.27), +(164, 77, 24, 2, 23.96), +(165, 77, 41, 1, 11.04), +(166, 78, 29, 1, 45.73), +(167, 78, 46, 2, 40.15), +(168, 78, 63, 1, 15.49), +(169, 79, 34, 2, 59.42), +(170, 80, 39, 1, 122.98), +(171, 80, 56, 2, 36.76), +(172, 81, 44, 2, 28.51), +(173, 81, 61, 1, 10.69), +(174, 81, 78, 2, 149.99), +(175, 82, 49, 1, 57.62), +(176, 83, 54, 2, 27.69), +(177, 83, 71, 1, 37.99), +(178, 84, 59, 1, 50.36), +(179, 84, 76, 2, 117.99), +(180, 84, 93, 1, 10.91), +(181, 85, 64, 2, 17.89), +(182, 86, 69, 1, 29.89), +(183, 86, 86, 2, 53.55), +(184, 87, 74, 2, 85.99), +(185, 87, 91, 1, 7.44), +(186, 87, 8, 2, 63.05), +(187, 88, 79, 1, 165.99), +(188, 89, 84, 2, 39.32), +(189, 89, 1, 1, 25.09), +(190, 90, 89, 1, 74.88), +(191, 90, 6, 2, 52.20), +(192, 90, 23, 1, 19.60), +(193, 91, 94, 2, 12.64), +(194, 92, 99, 1, 21.31), +(195, 92, 16, 2, 18.60), +(196, 93, 4, 2, 41.36), +(197, 93, 21, 1, 10.89), +(198, 93, 38, 2, 110.27), +(199, 94, 9, 1, 68.47), +(200, 95, 14, 2, 14.16), +(201, 95, 31, 1, 21.29), +(202, 96, 1, 1, 25.09), +(203, 96, 12, 1, 9.71), +(204, 97, 8, 1, 63.05), +(205, 97, 19, 1, 25.27), +(206, 98, 15, 1, 16.38), +(207, 98, 26, 1, 32.67), +(208, 99, 22, 1, 15.25), +(209, 100, 29, 1, 45.73), +(210, 100, 40, 1, 135.69), +(211, 101, 36, 1, 84.85), +(212, 101, 47, 1, 45.97), +(213, 102, 43, 1, 22.68), +(214, 102, 54, 1, 27.69), +(215, 103, 50, 1, 63.44), +(216, 103, 61, 1, 10.69), +(217, 104, 57, 1, 41.29), +(218, 104, 68, 1, 27.49), +(219, 105, 64, 1, 17.89), +(220, 106, 71, 1, 37.99), +(221, 106, 82, 1, 25.10), +(222, 107, 78, 1, 149.99), +(223, 108, 4, 1, 41.36), +(224, 109, 9, 1, 68.47), +(225, 110, 14, 1, 14.16); + +INSERT INTO refund (id, saleId, customerId, amount, reason, status) VALUES +(1, 1, 3, 34.80, 'Product was unsuitable after purchase.', 'APPROVED'), +(2, 2, 4, 88.32, 'Duplicate item purchased in error.', 'APPROVED'), +(3, 3, 5, 49.05, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(4, 4, 6, 15.25, 'Food sensitivity required a different formula.', 'APPROVED'), +(5, 5, 7, 181.42, 'Customer reported damaged packaging.', 'APPROVED'), +(6, 6, 8, 130.82, 'Product was unsuitable after purchase.', 'APPROVED'), +(7, 7, 9, 50.37, 'Duplicate item purchased in error.', 'APPROVED'), +(8, 8, 10, 74.13, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(9, 9, 11, 68.78, 'Food sensitivity required a different formula.', 'APPROVED'), +(10, 10, 12, 17.89, 'Customer reported damaged packaging.', 'APPROVED'), +(11, 11, 13, 63.09, 'Product was unsuitable after purchase.', 'APPROVED'), +(12, 12, 14, 149.99, 'Duplicate item purchased in error.', 'APPROVED'), +(13, 13, 15, 41.36, 'Pet outgrew the item sooner than expected.', 'APPROVED'), +(14, 14, 16, 68.47, 'Food sensitivity required a different formula.', 'APPROVED'), +(15, 15, 17, 14.16, 'Customer reported damaged packaging.', 'APPROVED'); + +INSERT INTO refund_item (id, refund_id, prod_id, quantity, unit_price) VALUES +(1, 1, 1, 1, 25.09), +(2, 1, 12, 1, 9.71), +(3, 2, 8, 1, 63.05), +(4, 2, 19, 1, 25.27), +(5, 3, 15, 1, 16.38), +(6, 3, 26, 1, 32.67), +(7, 4, 22, 1, 15.25), +(8, 5, 29, 1, 45.73), +(9, 5, 40, 1, 135.69), +(10, 6, 36, 1, 84.85), +(11, 6, 47, 1, 45.97), +(12, 7, 43, 1, 22.68), +(13, 7, 54, 1, 27.69), +(14, 8, 50, 1, 63.44), +(15, 8, 61, 1, 10.69), +(16, 9, 57, 1, 41.29), +(17, 9, 68, 1, 27.49), +(18, 10, 64, 1, 17.89), +(19, 11, 71, 1, 37.99), +(20, 11, 82, 1, 25.10), +(21, 12, 78, 1, 149.99), +(22, 13, 4, 1, 41.36), +(23, 14, 9, 1, 68.47), +(24, 15, 14, 1, 14.16); + +INSERT INTO conversation (id, customerId, staffId, status, mode, humanRequestedAt) VALUES +(1, 16, 3, 'CLOSED', 'AUTOMATED', NULL), +(2, 17, 4, 'OPEN', 'HUMAN', '2026-02-02 09:08:00'), +(3, 18, 5, 'OPEN', 'HUMAN', '2026-02-03 09:08:00'), +(4, 19, 6, 'OPEN', 'AUTOMATED', NULL), +(5, 20, 7, 'CLOSED', 'HUMAN', '2026-02-05 09:08:00'), +(6, 21, 8, 'OPEN', 'HUMAN', '2026-02-06 09:08:00'), +(7, 22, 9, 'OPEN', 'AUTOMATED', NULL), +(8, 23, 10, 'OPEN', 'HUMAN', '2026-02-08 09:08:00'), +(9, 24, 11, 'CLOSED', 'HUMAN', '2026-02-09 09:08:00'), +(10, 25, 12, 'OPEN', 'AUTOMATED', NULL), +(11, 26, 13, 'OPEN', 'HUMAN', '2026-02-11 09:08:00'), +(12, 27, 14, 'OPEN', 'HUMAN', '2026-02-12 09:08:00'), +(13, 28, 3, 'CLOSED', 'AUTOMATED', NULL), +(14, 29, 4, 'OPEN', 'HUMAN', '2026-02-14 09:08:00'), +(15, 30, 5, 'OPEN', 'HUMAN', '2026-02-15 09:08:00'), +(16, 31, 6, 'OPEN', 'AUTOMATED', NULL), +(17, 32, 7, 'CLOSED', 'HUMAN', '2026-02-17 09:08:00'), +(18, 33, 8, 'OPEN', 'HUMAN', '2026-02-18 09:08:00'), +(19, 34, 9, 'OPEN', 'AUTOMATED', NULL), +(20, 35, 10, 'OPEN', 'HUMAN', '2026-02-20 09:08:00'), +(21, 36, 11, 'CLOSED', 'HUMAN', '2026-02-21 09:08:00'), +(22, 37, 12, 'OPEN', 'AUTOMATED', NULL), +(23, 38, 13, 'OPEN', 'HUMAN', '2026-02-23 09:08:00'), +(24, 39, 14, 'OPEN', 'HUMAN', '2026-02-24 09:08:00'), +(25, 40, 3, 'CLOSED', 'AUTOMATED', NULL), +(26, 41, 4, 'OPEN', 'HUMAN', '2026-02-26 09:08:00'), +(27, 42, 5, 'OPEN', 'HUMAN', '2026-02-27 09:08:00'), +(28, 43, 6, 'OPEN', 'AUTOMATED', NULL), +(29, 44, 7, 'CLOSED', 'HUMAN', '2026-03-01 09:08:00'), +(30, 45, 8, 'OPEN', 'HUMAN', '2026-03-02 09:08:00'); + +INSERT INTO message (id, conversationId, senderId, content, attachmentUrl, attachmentName, attachmentMimeType, attachmentSizeBytes, timestamp, isRead) VALUES +(1, 1, 16, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-01 09:00:00', 1), +(2, 1, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-01 09:05:00', 1), +(3, 1, 16, 'Order #1001 is the one I meant, and the pet is Bruno.', 'https://files.petshop.local/chat/001-2.pdf', 'order-note-001.pdf', 'application/pdf', 145000, '2026-02-01 09:10:00', 1), +(4, 1, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-01 09:15:00', 0), +(5, 2, 17, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-02 09:00:00', 1), +(6, 2, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-02 09:05:00', 1), +(7, 2, 17, 'Order #1002 is the one I meant, and the pet is Snowball.', NULL, NULL, NULL, NULL, '2026-02-02 09:10:00', 1), +(8, 2, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-02 09:15:00', 0), +(9, 3, 18, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-03 09:00:00', 1), +(10, 3, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-03 09:05:00', 1), +(11, 3, 18, 'Order #1003 is the one I meant, and the pet is Zeus.', NULL, NULL, NULL, NULL, '2026-02-03 09:10:00', 1), +(12, 3, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-03 09:15:00', 0), +(13, 4, 19, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-04 09:00:00', 1), +(14, 4, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-04 09:05:00', 1), +(15, 4, 19, 'Order #1004 is the one I meant, and the pet is Biscuit.', NULL, NULL, NULL, NULL, '2026-02-04 09:10:00', 1), +(16, 4, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-04 09:15:00', 0), +(17, 5, 20, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-05 09:00:00', 1), +(18, 5, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-05 09:05:00', 1), +(19, 5, 20, 'Order #1005 is the one I meant, and the pet is Patches.', NULL, NULL, NULL, NULL, '2026-02-05 09:10:00', 1), +(20, 5, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-05 09:15:00', 0), +(21, 6, 21, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-06 09:00:00', 1), +(22, 6, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-06 09:05:00', 1), +(23, 6, 21, 'Order #1006 is the one I meant, and the pet is Scout.', 'https://files.petshop.local/chat/006-2.pdf', 'order-note-006.pdf', 'application/pdf', 145500, '2026-02-06 09:10:00', 1), +(24, 6, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-06 09:15:00', 0), +(25, 7, 22, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-07 09:00:00', 1), +(26, 7, 9, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-07 09:05:00', 1), +(27, 7, 22, 'Order #1007 is the one I meant, and the pet is Mittens.', NULL, NULL, NULL, NULL, '2026-02-07 09:10:00', 1), +(28, 7, 9, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-07 09:15:00', 0), +(29, 8, 23, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-08 09:00:00', 1), +(30, 8, 10, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-08 09:05:00', 1), +(31, 8, 23, 'Order #1008 is the one I meant, and the pet is Thor.', NULL, NULL, NULL, NULL, '2026-02-08 09:10:00', 1), +(32, 8, 10, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-08 09:15:00', 0), +(33, 9, 24, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-09 09:00:00', 1), +(34, 9, 11, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-09 09:05:00', 1), +(35, 9, 24, 'Order #1009 is the one I meant, and the pet is Whiskers.', NULL, NULL, NULL, NULL, '2026-02-09 09:10:00', 1), +(36, 9, 11, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-09 09:15:00', 0), +(37, 10, 25, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-10 09:00:00', 1), +(38, 10, 12, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-10 09:05:00', 1), +(39, 10, 25, 'Order #1010 is the one I meant, and the pet is Goldie.', NULL, NULL, NULL, NULL, '2026-02-10 09:10:00', 1), +(40, 10, 12, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-10 09:15:00', 0), +(41, 11, 26, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-11 09:00:00', 1), +(42, 11, 13, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-11 09:05:00', 1), +(43, 11, 26, 'Order #1011 is the one I meant, and the pet is Midnight.', 'https://files.petshop.local/chat/011-2.pdf', 'order-note-011.pdf', 'application/pdf', 146000, '2026-02-11 09:10:00', 1), +(44, 11, 13, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-11 09:15:00', 0), +(45, 12, 27, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-12 09:00:00', 1), +(46, 12, 14, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-12 09:05:00', 1), +(47, 12, 27, 'Order #1012 is the one I meant, and the pet is Storm.', NULL, NULL, NULL, NULL, '2026-02-12 09:10:00', 1), +(48, 12, 14, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-12 09:15:00', 0), +(49, 13, 28, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-13 09:00:00', 1), +(50, 13, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-13 09:05:00', 1), +(51, 13, 28, 'Order #1013 is the one I meant, and the pet is Peanut.', NULL, NULL, NULL, NULL, '2026-02-13 09:10:00', 1), +(52, 13, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-13 09:15:00', 0), +(53, 14, 29, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-14 09:00:00', 1), +(54, 14, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-14 09:05:00', 1), +(55, 14, 29, 'Order #1014 is the one I meant, and the pet is Daisy.', NULL, NULL, NULL, NULL, '2026-02-14 09:10:00', 1), +(56, 14, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-14 09:15:00', 0), +(57, 15, 30, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-15 09:00:00', 1), +(58, 15, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-15 09:05:00', 1), +(59, 15, 30, 'Order #1015 is the one I meant, and the pet is Cleo.', NULL, NULL, NULL, NULL, '2026-02-15 09:10:00', 1), +(60, 15, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-15 09:15:00', 0), +(61, 16, 31, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-16 09:00:00', 1), +(62, 16, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-16 09:05:00', 1), +(63, 16, 31, 'Order #1016 is the one I meant, and the pet is Sunny.', 'https://files.petshop.local/chat/016-2.pdf', 'order-note-016.pdf', 'application/pdf', 146500, '2026-02-16 09:10:00', 1), +(64, 16, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-16 09:15:00', 0), +(65, 17, 32, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-17 09:00:00', 1), +(66, 17, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-17 09:05:00', 1), +(67, 17, 32, 'Order #1017 is the one I meant, and the pet is Maple.', NULL, NULL, NULL, NULL, '2026-02-17 09:10:00', 1), +(68, 17, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-17 09:15:00', 0), +(69, 18, 33, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-18 09:00:00', 1), +(70, 18, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-18 09:05:00', 1), +(71, 18, 33, 'Order #1018 is the one I meant, and the pet is Nova.', NULL, NULL, NULL, NULL, '2026-02-18 09:10:00', 1), +(72, 18, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-18 09:15:00', 0), +(73, 19, 34, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-19 09:00:00', 1), +(74, 19, 9, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-19 09:05:00', 1), +(75, 19, 34, 'Order #1019 is the one I meant, and the pet is Piper.', NULL, NULL, NULL, NULL, '2026-02-19 09:10:00', 1), +(76, 19, 9, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-19 09:15:00', 0), +(77, 20, 35, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-20 09:00:00', 1), +(78, 20, 10, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-20 09:05:00', 1), +(79, 20, 35, 'Order #1020 is the one I meant, and the pet is Hazel.', NULL, NULL, NULL, NULL, '2026-02-20 09:10:00', 1), +(80, 20, 10, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-20 09:15:00', 0), +(81, 21, 36, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-21 09:00:00', 1), +(82, 21, 11, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-21 09:05:00', 1), +(83, 21, 36, 'Order #1021 is the one I meant, and the pet is Jasper.', 'https://files.petshop.local/chat/021-2.pdf', 'order-note-021.pdf', 'application/pdf', 147000, '2026-02-21 09:10:00', 1), +(84, 21, 11, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-21 09:15:00', 0), +(85, 22, 37, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-22 09:00:00', 1), +(86, 22, 12, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-22 09:05:00', 1), +(87, 22, 37, 'Order #1022 is the one I meant, and the pet is Remy.', NULL, NULL, NULL, NULL, '2026-02-22 09:10:00', 1), +(88, 22, 12, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-22 09:15:00', 0), +(89, 23, 38, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-23 09:00:00', 1), +(90, 23, 13, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-23 09:05:00', 1), +(91, 23, 38, 'Order #1023 is the one I meant, and the pet is Archie.', NULL, NULL, NULL, NULL, '2026-02-23 09:10:00', 1), +(92, 23, 13, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-23 09:15:00', 0), +(93, 24, 39, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-24 09:00:00', 1), +(94, 24, 14, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-24 09:05:00', 1), +(95, 24, 39, 'Order #1024 is the one I meant, and the pet is Skye.', NULL, NULL, NULL, NULL, '2026-02-24 09:10:00', 1), +(96, 24, 14, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-24 09:15:00', 0), +(97, 25, 40, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-25 09:00:00', 1), +(98, 25, 3, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-25 09:05:00', 1), +(99, 25, 40, 'Order #1025 is the one I meant, and the pet is Otis.', NULL, NULL, NULL, NULL, '2026-02-25 09:10:00', 1), +(100, 25, 3, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-25 09:15:00', 0), +(101, 26, 41, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-26 09:00:00', 1), +(102, 26, 4, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-26 09:05:00', 1), +(103, 26, 41, 'Order #1026 is the one I meant, and the pet is Marley.', 'https://files.petshop.local/chat/026-2.pdf', 'order-note-026.pdf', 'application/pdf', 147500, '2026-02-26 09:10:00', 1), +(104, 26, 4, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-26 09:15:00', 0), +(105, 27, 42, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-27 09:00:00', 1), +(106, 27, 5, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-27 09:05:00', 1), +(107, 27, 42, 'Order #1027 is the one I meant, and the pet is Blue.', NULL, NULL, NULL, NULL, '2026-02-27 09:10:00', 1), +(108, 27, 5, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-27 09:15:00', 0), +(109, 28, 43, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-02-28 09:00:00', 1), +(110, 28, 6, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-02-28 09:05:00', 1), +(111, 28, 43, 'Order #1028 is the one I meant, and the pet is Honey.', NULL, NULL, NULL, NULL, '2026-02-28 09:10:00', 1), +(112, 28, 6, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-02-28 09:15:00', 0), +(113, 29, 44, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-03-01 09:00:00', 1), +(114, 29, 7, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-01 09:05:00', 1), +(115, 29, 44, 'Order #1029 is the one I meant, and the pet is Mochi.', NULL, NULL, NULL, NULL, '2026-03-01 09:10:00', 1), +(116, 29, 7, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-01 09:15:00', 0), +(117, 30, 45, 'Hi, I need help with a recent order and a pet care question.', NULL, NULL, NULL, NULL, '2026-03-02 09:00:00', 1), +(118, 30, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-02 09:05:00', 1), +(119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1), +(120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0); + +INSERT INTO activityLog (logId, userId, storeId, activity, logTimestamp) VALUES +(1, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-03 08:00:00'), +(2, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-03 17:00:00'), +(3, 3, 1, 'Updated a pet availability record.', '2026-01-04 02:00:00'), +(4, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-04 11:00:00'), +(5, 5, 1, 'Checked a pending adoption record.', '2026-01-04 20:00:00'), +(6, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-05 05:00:00'), +(7, 7, 2, 'Answered a customer support conversation.', '2026-01-05 14:00:00'), +(8, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-05 23:00:00'), +(9, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-06 08:00:00'), +(10, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-06 17:00:00'), +(11, 11, 3, 'Updated a pet availability record.', '2026-01-07 02:00:00'), +(12, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-07 11:00:00'), +(13, 13, 3, 'Checked a pending adoption record.', '2026-01-07 20:00:00'), +(14, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-08 05:00:00'), +(15, 1, 1, 'Answered a customer support conversation.', '2026-01-08 14:00:00'), +(16, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-08 23:00:00'), +(17, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-09 08:00:00'), +(18, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-09 17:00:00'), +(19, 5, 1, 'Updated a pet availability record.', '2026-01-10 02:00:00'), +(20, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-10 11:00:00'), +(21, 7, 2, 'Checked a pending adoption record.', '2026-01-10 20:00:00'), +(22, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-11 05:00:00'), +(23, 9, 2, 'Answered a customer support conversation.', '2026-01-11 14:00:00'), +(24, 10, 2, 'Updated a product detail for the catalogue.', '2026-01-11 23:00:00'), +(25, 11, 3, 'Reviewed store inventory adjustments.', '2026-01-12 08:00:00'), +(26, 12, 3, 'Approved a purchase transaction at the register.', '2026-01-12 17:00:00'), +(27, 13, 3, 'Updated a pet availability record.', '2026-01-13 02:00:00'), +(28, 14, 3, 'Completed a grooming appointment handoff.', '2026-01-13 11:00:00'), +(29, 1, 1, 'Checked a pending adoption record.', '2026-01-13 20:00:00'), +(30, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-14 05:00:00'), +(31, 3, 1, 'Answered a customer support conversation.', '2026-01-14 14:00:00'), +(32, 4, 1, 'Updated a product detail for the catalogue.', '2026-01-14 23:00:00'), +(33, 5, 1, 'Reviewed store inventory adjustments.', '2026-01-15 08:00:00'), +(34, 6, 1, 'Approved a purchase transaction at the register.', '2026-01-15 17:00:00'), +(35, 7, 2, 'Updated a pet availability record.', '2026-01-16 02:00:00'), +(36, 8, 2, 'Completed a grooming appointment handoff.', '2026-01-16 11:00:00'), +(37, 9, 2, 'Checked a pending adoption record.', '2026-01-16 20:00:00'), +(38, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-17 05:00:00'), +(39, 11, 3, 'Answered a customer support conversation.', '2026-01-17 14:00:00'), +(40, 12, 3, 'Updated a product detail for the catalogue.', '2026-01-17 23:00:00'), +(41, 13, 3, 'Reviewed store inventory adjustments.', '2026-01-18 08:00:00'), +(42, 14, 3, 'Approved a purchase transaction at the register.', '2026-01-18 17:00:00'), +(43, 1, 1, 'Updated a pet availability record.', '2026-01-19 02:00:00'), +(44, 2, 2, 'Completed a grooming appointment handoff.', '2026-01-19 11:00:00'), +(45, 3, 1, 'Checked a pending adoption record.', '2026-01-19 20:00:00'), +(46, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-20 05:00:00'), +(47, 5, 1, 'Answered a customer support conversation.', '2026-01-20 14:00:00'), +(48, 6, 1, 'Updated a product detail for the catalogue.', '2026-01-20 23:00:00'), +(49, 7, 2, 'Reviewed store inventory adjustments.', '2026-01-21 08:00:00'), +(50, 8, 2, 'Approved a purchase transaction at the register.', '2026-01-21 17:00:00'), +(51, 9, 2, 'Updated a pet availability record.', '2026-01-22 02:00:00'), +(52, 10, 2, 'Completed a grooming appointment handoff.', '2026-01-22 11:00:00'), +(53, 11, 3, 'Checked a pending adoption record.', '2026-01-22 20:00:00'), +(54, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-23 05:00:00'), +(55, 13, 3, 'Answered a customer support conversation.', '2026-01-23 14:00:00'), +(56, 14, 3, 'Updated a product detail for the catalogue.', '2026-01-23 23:00:00'), +(57, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-24 08:00:00'), +(58, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-24 17:00:00'), +(59, 3, 1, 'Updated a pet availability record.', '2026-01-25 02:00:00'), +(60, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-25 11:00:00'), +(61, 5, 1, 'Checked a pending adoption record.', '2026-01-25 20:00:00'), +(62, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-26 05:00:00'), +(63, 7, 2, 'Answered a customer support conversation.', '2026-01-26 14:00:00'), +(64, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-26 23:00:00'), +(65, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-27 08:00:00'), +(66, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-27 17:00:00'), +(67, 11, 3, 'Updated a pet availability record.', '2026-01-28 02:00:00'), +(68, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-28 11:00:00'), +(69, 13, 3, 'Checked a pending adoption record.', '2026-01-28 20:00:00'), +(70, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-29 05:00:00'), +(71, 1, 1, 'Answered a customer support conversation.', '2026-01-29 14:00:00'), +(72, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-29 23:00:00'), +(73, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-30 08:00:00'), +(74, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-30 17:00:00'), +(75, 5, 1, 'Updated a pet availability record.', '2026-01-31 02:00:00'), +(76, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-31 11:00:00'), +(77, 7, 2, 'Checked a pending adoption record.', '2026-01-31 20:00:00'), +(78, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-01 05:00:00'), +(79, 9, 2, 'Answered a customer support conversation.', '2026-02-01 14:00:00'), +(80, 10, 2, 'Updated a product detail for the catalogue.', '2026-02-01 23:00:00'), +(81, 11, 3, 'Reviewed store inventory adjustments.', '2026-02-02 08:00:00'), +(82, 12, 3, 'Approved a purchase transaction at the register.', '2026-02-02 17:00:00'), +(83, 13, 3, 'Updated a pet availability record.', '2026-02-03 02:00:00'), +(84, 14, 3, 'Completed a grooming appointment handoff.', '2026-02-03 11:00:00'), +(85, 1, 1, 'Checked a pending adoption record.', '2026-02-03 20:00:00'), +(86, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-04 05:00:00'), +(87, 3, 1, 'Answered a customer support conversation.', '2026-02-04 14:00:00'), +(88, 4, 1, 'Updated a product detail for the catalogue.', '2026-02-04 23:00:00'), +(89, 5, 1, 'Reviewed store inventory adjustments.', '2026-02-05 08:00:00'), +(90, 6, 1, 'Approved a purchase transaction at the register.', '2026-02-05 17:00:00'), +(91, 7, 2, 'Updated a pet availability record.', '2026-02-06 02:00:00'), +(92, 8, 2, 'Completed a grooming appointment handoff.', '2026-02-06 11:00:00'), +(93, 9, 2, 'Checked a pending adoption record.', '2026-02-06 20:00:00'), +(94, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-07 05:00:00'), +(95, 11, 3, 'Answered a customer support conversation.', '2026-02-07 14:00:00'), +(96, 12, 3, 'Updated a product detail for the catalogue.', '2026-02-07 23:00:00'), +(97, 13, 3, 'Reviewed store inventory adjustments.', '2026-02-08 08:00:00'), +(98, 14, 3, 'Approved a purchase transaction at the register.', '2026-02-08 17:00:00'), +(99, 1, 1, 'Updated a pet availability record.', '2026-02-09 02:00:00'), +(100, 2, 2, 'Completed a grooming appointment handoff.', '2026-02-09 11:00:00'), +(101, 3, 1, 'Checked a pending adoption record.', '2026-02-09 20:00:00'), +(102, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-10 05:00:00'), +(103, 5, 1, 'Answered a customer support conversation.', '2026-02-10 14:00:00'), +(104, 6, 1, 'Updated a product detail for the catalogue.', '2026-02-10 23:00:00'), +(105, 7, 2, 'Reviewed store inventory adjustments.', '2026-02-11 08:00:00'), +(106, 8, 2, 'Approved a purchase transaction at the register.', '2026-02-11 17:00:00'), +(107, 9, 2, 'Updated a pet availability record.', '2026-02-12 02:00:00'), +(108, 10, 2, 'Completed a grooming appointment handoff.', '2026-02-12 11:00:00'), +(109, 11, 3, 'Checked a pending adoption record.', '2026-02-12 20:00:00'), +(110, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-02-13 05:00:00'), +(111, 13, 3, 'Answered a customer support conversation.', '2026-02-13 14:00:00'), +(112, 14, 3, 'Updated a product detail for the catalogue.', '2026-02-13 23:00:00'), +(113, 1, 1, 'Reviewed store inventory adjustments.', '2026-02-14 08:00:00'), +(114, 2, 2, 'Approved a purchase transaction at the register.', '2026-02-14 17:00:00'), +(115, 3, 1, 'Updated a pet availability record.', '2026-02-15 02:00:00'), +(116, 4, 1, 'Completed a grooming appointment handoff.', '2026-02-15 11:00:00'), +(117, 5, 1, 'Checked a pending adoption record.', '2026-02-15 20:00:00'), +(118, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-16 05:00:00'), +(119, 7, 2, 'Answered a customer support conversation.', '2026-02-16 14:00:00'), +(120, 8, 2, 'Updated a product detail for the catalogue.', '2026-02-16 23:00:00'); From 12c7384951643ac28c5540e4a419a15d42d5fd8c Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:10:42 -0600 Subject: [PATCH 100/137] Fixed backend missing file issue --- .../com/petshop/backend/service/AvatarStorageService.java | 5 ++++- .../main/java/com/petshop/backend/service/PetService.java | 2 +- .../java/com/petshop/backend/service/ProductService.java | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java index 952ca600..dff64508 100644 --- a/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java +++ b/backend/src/main/java/com/petshop/backend/service/AvatarStorageService.java @@ -48,7 +48,10 @@ public class AvatarStorageService { if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) { return; } - Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl())); + try { + Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl())); + } catch (IllegalArgumentException ignored) { + } } public String toOwnerAvatarUrl(User user) { diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index ef6ab4c5..09dd402b 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -231,7 +231,7 @@ public class PetService { } try { catalogImageStorageService.deletePetImage(storedImagePath); - } catch (IOException ignored) { + } catch (IOException | IllegalArgumentException ignored) { } } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 8d4b1da2..d18890d5 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -143,7 +143,7 @@ public class ProductService { } try { catalogImageStorageService.deleteProductImage(storedImagePath); - } catch (IOException ignored) { + } catch (IOException | IllegalArgumentException ignored) { } } From fdc6f624413395117c71dfa0c31facd0fd735812 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:27:17 -0600 Subject: [PATCH 101/137] fixed pet DTO and how it interacts with new backend --- .../petstoremobile/adapters/PetAdapter.java | 8 +++---- .../example/petstoremobile/dtos/PetDTO.java | 22 ++++++++++++++++--- .../detailfragments/PetDetailFragment.java | 10 ++++++--- .../PetProfileFragment.java | 7 +++--- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 35c323eb..668f8f30 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -74,11 +74,11 @@ public class PetAdapter extends RecyclerView.Adapter { holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); - try { - double price = Double.parseDouble(pet.getPetPrice()); + Double price = pet.getPetPrice(); + if (price != null) { holder.tvPetPrice.setText("$" + String.format("%.2f", price)); - } catch (Exception e) { - holder.tvPetPrice.setText("$" + pet.getPetPrice()); + } else { + holder.tvPetPrice.setText("$0.00"); } holder.tvPetStatus.setText(pet.getPetStatus()); diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java index d76a8509..0e9a0b3f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java @@ -7,9 +7,13 @@ public class PetDTO { private String petBreed; private Integer petAge; private String petStatus; - private String petPrice; + private Double petPrice; private String createdAt; private String updatedAt; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; public Long getPetId() { return petId; } public void setPetId(Long petId) { this.petId = petId; } @@ -29,12 +33,24 @@ public class PetDTO { public String getPetStatus() { return petStatus; } public void setPetStatus(String petStatus) { this.petStatus = petStatus; } - public String getPetPrice() { return petPrice; } - public void setPetPrice(String petPrice) { this.petPrice = petPrice; } + public Double getPetPrice() { return petPrice; } + public void setPetPrice(Double petPrice) { this.petPrice = petPrice; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } + + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } + + public String getCustomerName() { return customerName; } + public void setCustomerName(String customerName) { this.customerName = customerName; } + + public Long getStoreId() { return storeId; } + public void setStoreId(Long storeId) { this.storeId = storeId; } + + public String getStoreName() { return storeName; } + public void setStoreName(String storeName) { this.storeName = storeName; } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index eb34869c..57c0ac18 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -23,6 +23,8 @@ import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.PetViewModel; +import java.util.Locale; + import dagger.hilt.android.AndroidEntryPoint; /** @@ -85,7 +87,7 @@ public class PetDetailFragment extends Fragment { String species = binding.etPetSpecies.getText().toString().trim(); String breed = binding.etPetBreed.getText().toString().trim(); int age = Integer.parseInt(binding.etPetAge.getText().toString().trim()); - String priceStr = binding.etPetPrice.getText().toString().trim(); + double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); //create a pet object to send to the API @@ -94,7 +96,7 @@ public class PetDetailFragment extends Fragment { petDTO.setPetSpecies(species); petDTO.setPetBreed(breed); petDTO.setPetAge(age); - petDTO.setPetPrice(priceStr); + petDTO.setPetPrice(price); petDTO.setPetStatus(status); //check if the pet is being edited or added @@ -191,7 +193,9 @@ public class PetDetailFragment extends Fragment { binding.etPetSpecies.setText(p.getPetSpecies()); binding.etPetBreed.setText(p.getPetBreed()); binding.etPetAge.setText(String.valueOf(p.getPetAge())); - binding.etPetPrice.setText(p.getPetPrice()); + if (p.getPetPrice() != null) { + binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); + } SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus()); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 69391412..68fb3eec 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -124,9 +124,10 @@ public class PetProfileFragment extends Fragment { binding.tvPetSpecies.setText(pet.getPetSpecies()); binding.tvPetBreed.setText(pet.getPetBreed()); binding.tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", pet.getPetAge())); - try { - binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", Double.parseDouble(pet.getPetPrice()))); - } catch (Exception e) { + + if (pet.getPetPrice() != null) { + binding.tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", pet.getPetPrice())); + } else { binding.tvPetPrice.setText("$0.00"); } } else if (resource.status == Resource.Status.ERROR) { From 867322b46223cb93a2f444aabb9436408d88ccd7 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:23:58 -0600 Subject: [PATCH 102/137] changed petDetailFragment to support new backend --- .../petstoremobile/dtos/CustomerDTO.java | 29 +++- .../detailfragments/PetDetailFragment.java | 154 +++++++++++++++++- .../PetProfileFragment.java | 7 + .../petstoremobile/utils/InputValidator.java | 20 +++ .../petstoremobile/utils/SpinnerUtils.java | 4 +- .../main/res/layout/fragment_pet_detail.xml | 28 ++++ .../main/res/layout/fragment_pet_profile.xml | 31 +++- 7 files changed, 269 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java index 178b0033..21376a63 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -1,6 +1,9 @@ package com.example.petstoremobile.dtos; +import com.google.gson.annotations.SerializedName; + public class CustomerDTO { + @SerializedName("id") private Long customerId; private String firstName; private String lastName; @@ -12,18 +15,34 @@ public class CustomerDTO { return customerId; } + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { + this.lastName = lastName; + } + public String getEmail() { return email; } + public void setEmail(String email) { + this.email = email; + } + public String getFullName() { return firstName + " " + lastName; } @@ -32,7 +51,15 @@ public class CustomerDTO { return createdAt; } + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + public String getUpdatedAt() { return updatedAt; } -} \ No newline at end of file + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java index 57c0ac18..62d8bf42 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -11,18 +11,27 @@ import androidx.navigation.fragment.NavHostFragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Spinner; +import android.widget.TextView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.databinding.FragmentPetDetailBinding; +import com.example.petstoremobile.dtos.CustomerDTO; import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DialogUtils; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; +import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import dagger.hilt.android.AndroidEntryPoint; @@ -38,11 +47,19 @@ public class PetDetailFragment extends Fragment { private boolean isEditing = false; private PetViewModel viewModel; + private CustomerViewModel customerViewModel; + private StoreViewModel storeViewModel; + private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); + private Long selectedCustomerId = null; + private Long selectedStoreId = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PetViewModel.class); + customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } @Override @@ -57,6 +74,8 @@ public class PetDetailFragment extends Fragment { super.onViewCreated(view, savedInstanceState); setupSpinner(); + loadCustomers(); + loadStores(); handleArguments(); //set button click listeners @@ -90,6 +109,36 @@ public class PetDetailFragment extends Fragment { double price = Double.parseDouble(binding.etPetPrice.getText().toString().trim()); String status = binding.spinnerPetStatus.getSelectedItem().toString(); + // Get selected customer + Long customerId = null; + int customerPos = binding.spinnerCustomer.getSelectedItemPosition(); + if (customerPos > 0) { // 0 means no customer for pet + customerId = customerList.get(customerPos - 1).getCustomerId(); + } + + // Get selected store + Long storeId = null; + int storePos = binding.spinnerStore.getSelectedItemPosition(); + if (storePos > 0) { + storeId = storeList.get(storePos - 1).getStoreId(); + } + + // Validation: If status is Available, a store must be selected + if ("Available".equalsIgnoreCase(status)) { + if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; + } + + // Validation: If status is Owned, an owner must be selected + if ("Owned".equalsIgnoreCase(status)) { + if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; + } + + // Validation: If status is Adopted, an owner and store must be selected + if ("Adopted".equalsIgnoreCase(status)) { + if (!InputValidator.isSpinnerSelected(binding.spinnerCustomer, "Owner")) return; + if (!InputValidator.isSpinnerSelected(binding.spinnerStore, "Store")) return; + } + //create a pet object to send to the API PetDTO petDTO = new PetDTO(); petDTO.setPetName(name); @@ -98,6 +147,8 @@ public class PetDetailFragment extends Fragment { petDTO.setPetAge(age); petDTO.setPetPrice(price); petDTO.setPetStatus(status); + petDTO.setCustomerId(customerId); + petDTO.setStoreId(storeId); //check if the pet is being edited or added if (isEditing) { @@ -197,17 +248,118 @@ public class PetDetailFragment extends Fragment { binding.etPetPrice.setText(String.format(Locale.getDefault(), "%.2f", p.getPetPrice())); } SpinnerUtils.setSelectionByValue(binding.spinnerPetStatus, p.getPetStatus()); + + selectedCustomerId = p.getCustomerId(); + updateCustomerSpinnerSelection(); + + selectedStoreId = p.getStoreId(); + updateStoreSpinnerSelection(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet: " + resource.message, Toast.LENGTH_SHORT).show(); } }); } + /** + * Fetches the list of customers and populates the spinner. + */ + private void loadCustomers() { + customerViewModel.getAllCustomers(0, 1000).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + customerList = resource.data.getContent(); + updateCustomerSpinnerSelection(); + } + }); + } + + /** + * Fetches the list of stores and populates the spinner. + */ + private void loadStores() { + storeViewModel.getAllStores(0, 1000).observe(getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + updateStoreSpinnerSelection(); + } + }); + } + + /** + * Updates the customer spinner with the current list and sets the selection if needed. + */ + private void updateCustomerSpinnerSelection() { + SpinnerUtils.populateSpinner( + requireContext(), + binding.spinnerCustomer, + customerList, + CustomerDTO::getFullName, + "No Owner", + selectedCustomerId, + CustomerDTO::getCustomerId + ); + } + + /** + * Updates the store spinner with the current list and sets the selection if needed. + */ + private void updateStoreSpinnerSelection() { + SpinnerUtils.populateSpinner( + requireContext(), + binding.spinnerStore, + storeList, + StoreDTO::getStoreName, + "None", + selectedStoreId, + StoreDTO::getStoreId + ); + } + /** * Initializes the spinner for pet status selection. */ private void setupSpinner() { SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerPetStatus, - new String[]{"Available", "Adopted"}); + new String[]{"Available", "Adopted", "Owned"}); + + binding.spinnerPetStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String status = parent.getItemAtPosition(position).toString(); + + // Clear any existing error icons when status changes + clearSpinnerError(binding.spinnerCustomer); + clearSpinnerError(binding.spinnerStore); + + //Disable the customer spinner if the status is "Available" + if ("Available".equalsIgnoreCase(status)) { + binding.spinnerCustomer.setSelection(0); + binding.spinnerCustomer.setEnabled(false); + } else { + binding.spinnerCustomer.setEnabled(true); + } + + //Disable the store spinner if the status is "Owned" + if ("Owned".equalsIgnoreCase(status)) { + binding.spinnerStore.setSelection(0); + binding.spinnerStore.setEnabled(false); + } else { + binding.spinnerStore.setEnabled(true); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + } + + /** + * Clears error messages from a Spinner's selected view. + */ + private void clearSpinnerError(Spinner spinner) { + View selectedView = spinner.getSelectedView(); + if (selectedView instanceof TextView) { + ((TextView) selectedView).setError(null); + } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 68fb3eec..9e876c69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -130,6 +130,13 @@ public class PetProfileFragment extends Fragment { } else { binding.tvPetPrice.setText("$0.00"); } + + // Display owner name if available, otherwise show No Owner + if (pet.getCustomerName() != null && !pet.getCustomerName().isEmpty()) { + binding.tvPetOwner.setText(pet.getCustomerName()); + } else { + binding.tvPetOwner.setText("No Owner"); + } } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load pet data: " + resource.message, Toast.LENGTH_SHORT).show(); } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java index d173df14..a10389b6 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -1,6 +1,9 @@ package com.example.petstoremobile.utils; +import android.view.View; import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; public class InputValidator { @@ -94,4 +97,21 @@ public class InputValidator { } return true; } + + /** + * Checks if a selection has been made in a Spinner. + * Assumes position 0 is a placeholder like "None" or "Select". + */ + public static boolean isSpinnerSelected(Spinner spinner, String fieldName) { + if (spinner.getSelectedItemPosition() <= 0) { + View selectedView = spinner.getSelectedView(); + if (selectedView instanceof TextView) { + TextView tv = (TextView) selectedView; + tv.setError(fieldName + " is required"); + spinner.requestFocus(); + } + return false; + } + return true; + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index 5095199e..fa22d21d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -8,6 +8,7 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Function; /** @@ -36,7 +37,8 @@ public class SpinnerUtils { if (preselectedId != null && preselectedId != -1) { int offset = (defaultText != null) ? 1 : 0; for (int i = 0; i < data.size(); i++) { - if (idExtractor.apply(data.get(i)).equals(preselectedId)) { + Long currentId = idExtractor.apply(data.get(i)); + if (Objects.equals(currentId, preselectedId)) { spinner.setSelection(i + offset); break; } diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml index 48558349..917a5de6 100644 --- a/android/app/src/main/res/layout/fragment_pet_detail.xml +++ b/android/app/src/main/res/layout/fragment_pet_detail.xml @@ -161,6 +161,34 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_pet_profile.xml b/android/app/src/main/res/layout/fragment_pet_profile.xml index b750bd29..f5af4445 100644 --- a/android/app/src/main/res/layout/fragment_pet_profile.xml +++ b/android/app/src/main/res/layout/fragment_pet_profile.xml @@ -221,6 +221,35 @@ + + + + + + + + @@ -241,4 +270,4 @@ - \ No newline at end of file + From 6164a8746d9fbc7b5c1e2e95a3e422ffc7d635be Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:46:00 -0600 Subject: [PATCH 103/137] changed filtering and search in pets to use api calls --- .../example/petstoremobile/api/PetApi.java | 9 +++- .../fragments/listfragments/PetFragment.java | 48 ++++++------------- .../AdoptionDetailFragment.java | 2 +- .../AppointmentDetailFragment.java | 2 +- .../repositories/PetRepository.java | 6 +-- .../viewmodels/PetViewModel.java | 6 +-- 6 files changed, 30 insertions(+), 43 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index ff7b79a7..7db2c1e3 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -20,11 +20,16 @@ public interface PetApi { // endpoint for downloading the pet's image file String PET_IMAGE_PATH = "api/v1/pets/%d/image"; - // Get all pets + // Get all pets with filters @GET("api/v1/pets") Call> getAllPets( @Query("page") int page, - @Query("size") int size + @Query("size") int size, + @Query("q") String query, + @Query("status") String status, + @Query("species") String species, + @Query("storeId") Long storeId, + @Query("sort") String sort ); // Get pet by id diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 01331b19..2aad5eb4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -38,10 +38,9 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; private List petList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); private PetAdapter adapter; private PetViewModel viewModel; - + @Inject @Named("baseUrl") String baseUrl; /** @@ -103,7 +102,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen binding.etSearchPet.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - filterPets(); + loadPetData(); } @Override public void afterTextChanged(Editable s) {} }); @@ -121,7 +120,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - filterPets(); + loadPetData(); } @Override @@ -129,30 +128,6 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen }); } - /** - * Filters the pet list based on both the search query and the selected status. - */ - private void filterPets() { - String query = binding.etSearchPet.getText().toString().toLowerCase(); - String selectedStatus = binding.spinnerStatus.getSelectedItem().toString(); - - filteredList.clear(); - for (PetDTO p : petList) { - boolean matchesSearch = query.isEmpty() || - p.getPetName().toLowerCase().contains(query) || - p.getPetSpecies().toLowerCase().contains(query) || - p.getPetBreed().toLowerCase().contains(query); - - boolean matchesStatus = selectedStatus.equals("All Statuses") || - p.getPetStatus().equalsIgnoreCase(selectedStatus); - - if (matchesSearch && matchesStatus) { - filteredList.add(p); - } - } - adapter.notifyDataSetChanged(); - } - /** * Sets up the SwipeRefreshLayout to allow manual re-fetching of pet data. */ @@ -165,7 +140,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen */ private void openPetProfile(int position) { Bundle args = new Bundle(); - PetDTO pet = filteredList.get(position); + PetDTO pet = petList.get(position); args.putLong("petId", pet.getPetId()); NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); @@ -187,10 +162,17 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } /** - * Fetches all pet data from the server via the ViewModel and updates the UI. + * Fetches pet data from the server with filters and updates the UI. */ private void loadPetData() { - viewModel.getAllPets(0, 100).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchPet.getText().toString().trim(); + String status = binding.spinnerStatus.getSelectedItem().toString(); + + if (status.equals("All Statuses")) { + status = null; + } + + viewModel.getAllPets(0, 100, query, status, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; switch (resource.status) { @@ -202,7 +184,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen if (resource.data != null) { petList.clear(); petList.addAll(resource.data.getContent()); - filterPets(); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -218,7 +200,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Initializes the RecyclerView with a layout manager and adapter for displaying pets. */ private void setupRecyclerView() { - adapter = new PetAdapter(filteredList, this); + adapter = new PetAdapter(petList, this); adapter.setBaseUrl(baseUrl); binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPets.setAdapter(adapter); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java index 1bdaba71..d38622f5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -113,7 +113,7 @@ public class AdoptionDetailFragment extends Fragment { * Loads the list of pets from the API. */ private void loadPets() { - petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { + petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); refreshPetSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index cc419a69..3b757b87 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -134,7 +134,7 @@ public class AppointmentDetailFragment extends Fragment { * Loads the list of pets from the ViewModel. */ private void loadPets() { - petViewModel.getAllPets(0, 200).observe(getViewLifecycleOwner(), resource -> { + petViewModel.getAllPets(0, 200, null, null, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { petList = resource.data.getContent(); refreshPetSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 64f23b78..88ac2295 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -23,10 +23,10 @@ public class PetRepository extends BaseRepository { } /** - * Retrieves a paginated list of all pets from the API. + * Retrieves a paginated list of pets from the API with optional filters. */ - public LiveData>> getAllPets(int page, int size) { - return executeCall(petApi.getAllPets(page, size)); + public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { + return executeCall(petApi.getAllPets(page, size, query, status, species, storeId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java index 8b0d5fcc..b0af57c8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -23,10 +23,10 @@ public class PetViewModel extends ViewModel { } /** - * Fetches a paginated list of all pets. + * Fetches a paginated list of pets with filters. */ - public LiveData>> getAllPets(int page, int size) { - return repository.getAllPets(page, size); + public LiveData>> getAllPets(int page, int size, String query, String status, String species, Long storeId, String sort) { + return repository.getAllPets(page, size, query, status, species, storeId, sort); } /** From 0086bb4a5e2b44d6669b67443e431ea8a4d818a7 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:24:55 -0600 Subject: [PATCH 104/137] added more filter options to pets --- .../fragments/listfragments/PetFragment.java | 115 +++++++++++++++--- .../app/src/main/res/layout/fragment_pet.xml | 58 +++++++-- 2 files changed, 141 insertions(+), 32 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index 2aad5eb4..bf6e7e4e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -23,8 +23,12 @@ import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.PetViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; import java.util.ArrayList; import java.util.List; @@ -38,22 +42,25 @@ import dagger.hilt.android.AndroidEntryPoint; public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { private FragmentPetBinding binding; private List petList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private PetAdapter adapter; private PetViewModel viewModel; + private StoreViewModel storeViewModel; @Inject @Named("baseUrl") String baseUrl; /** - * Initializes the fragment and its associated PetViewModel. + * Initializes the fragment and its associated ViewModels. */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PetViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** - * Sets up the fragment's UI components, including RecyclerView, search, status filter, and swipe-to-refresh. + * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -63,7 +70,10 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupRecyclerView(); setupSearch(); setupStatusFilter(); + setupSpeciesFilter(); + setupStoreFilter(); setupSwipeRefresh(); + setupFilterToggle(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -87,16 +97,38 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } /** - * Reloads pet data every time the fragment becomes visible. + * Reloads data every time the fragment becomes visible. */ @Override public void onResume() { super.onResume(); loadPetData(); + loadStoreData(); } /** - * Configures the search bar with a for filtering. + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchPet.setText(""); + binding.spinnerStatus.setSelection(0); + binding.spinnerSpecies.setSelection(0); + binding.spinnerStore.setSelection(0); + } + }); + } + + /** + * Configures the search bar. */ private void setupSearch() { binding.etSearchPet.addTextChangedListener(new TextWatcher() { @@ -122,57 +154,100 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen public void onItemSelected(AdapterView parent, View view, int position, long id) { loadPetData(); } - - @Override - public void onNothingSelected(AdapterView parent) {} + @Override public void onNothingSelected(AdapterView parent) {} }); } /** - * Sets up the SwipeRefreshLayout to allow manual re-fetching of pet data. + * Configures the species filter spinner with species. + */ + private void setupSpeciesFilter() { + String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; + BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, species); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.spinnerSpecies.setAdapter(adapter); + + binding.spinnerSpecies.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadPetData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Configures the store filter spinner. + */ + private void setupStoreFilter() { + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadPetData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Fetches store data to populate the store filter. + */ + private void loadStoreData() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + } + }); + } + + /** + * Sets up the SwipeRefreshLayout. */ private void setupSwipeRefresh() { binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData); } /** - * Navigates to the pet profile screen for a specific pet. + * Navigates to the pet profile screen. */ private void openPetProfile(int position) { Bundle args = new Bundle(); PetDTO pet = petList.get(position); args.putLong("petId", pet.getPetId()); - NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args); } /** - * Navigates to the pet detail screen. (Only used for adding a new pet on this screen) + * Navigates to the pet detail screen. */ private void openPetDetails() { NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail); } - /** - * Handles clicks on individual pet items in the list. - */ @Override public void onPetClick(int position) { openPetProfile(position); } /** - * Fetches pet data from the server with filters and updates the UI. + * Fetches pet data from the server with all active filters. */ private void loadPetData() { String query = binding.etSearchPet.getText().toString().trim(); - String status = binding.spinnerStatus.getSelectedItem().toString(); + String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; + String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species"; - if (status.equals("All Statuses")) { - status = null; + Long storeId = null; + if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { + storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); } - viewModel.getAllPets(0, 100, query, status, null, null, "petName").observe(getViewLifecycleOwner(), resource -> { + if (status.equals("All Statuses")) status = null; + if (species.equals("All Species")) species = null; + + viewModel.getAllPets(0, 100, query, status, species, storeId, "petName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; switch (resource.status) { @@ -197,7 +272,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen } /** - * Initializes the RecyclerView with a layout manager and adapter for displaying pets. + * Initializes the RecyclerView. */ private void setupRecyclerView() { adapter = new PetAdapter(petList, this); diff --git a/android/app/src/main/res/layout/fragment_pet.xml b/android/app/src/main/res/layout/fragment_pet.xml index 1f625bac..2e79ed1b 100644 --- a/android/app/src/main/res/layout/fragment_pet.xml +++ b/android/app/src/main/res/layout/fragment_pet.xml @@ -29,42 +29,76 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + + android:visibility="gone" + android:background="@color/white"> + + + + + + + + + android:layout_marginTop="4dp" + android:background="@android:color/transparent"/> + Date: Tue, 7 Apr 2026 03:51:09 -0600 Subject: [PATCH 105/137] Updated Filterdropdown design for pets --- .../adapters/WhiteTextArrayAdapter.java | 47 ++++++++++++ .../fragments/listfragments/PetFragment.java | 8 +- .../petstoremobile/utils/SpinnerUtils.java | 28 ++++++- .../src/main/res/drawable/bg_search_bar.xml | 6 ++ .../app/src/main/res/drawable/bg_spinner.xml | 7 ++ .../app/src/main/res/layout/fragment_pet.xml | 73 +++++++++++++------ 6 files changed, 142 insertions(+), 27 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/adapters/WhiteTextArrayAdapter.java create mode 100644 android/app/src/main/res/drawable/bg_search_bar.xml create mode 100644 android/app/src/main/res/drawable/bg_spinner.xml diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/WhiteTextArrayAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/WhiteTextArrayAdapter.java new file mode 100644 index 00000000..42042207 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/WhiteTextArrayAdapter.java @@ -0,0 +1,47 @@ +package com.example.petstoremobile.adapters; + +import android.content.Context; +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import com.example.petstoremobile.R; +import java.util.List; + +/** + * A class that overrides the arrayAdapter so the text color is white and background is transparent. + */ +public class WhiteTextArrayAdapter extends ArrayAdapter { + public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) { + super(context, resource, objects); + } + + public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + view.setBackgroundColor(Color.TRANSPARENT); + if (view instanceof TextView) { + ((TextView) view).setTextColor(Color.WHITE); + } + return view; + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getDropDownView(position, convertView, parent); + view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary_dark)); + if (view instanceof TextView) { + ((TextView) view).setTextColor(Color.WHITE); + } + return view; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index bf6e7e4e..bcb19fa8 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -19,8 +19,8 @@ import android.widget.AdapterView; import android.widget.Toast; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.PetAdapter; +import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; @@ -145,7 +145,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen */ private void setupStatusFilter() { String[] statuses = {"All Statuses", "Available", "Adopted"}; - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); + WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.spinnerStatus.setAdapter(adapter); @@ -163,7 +163,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen */ private void setupSpeciesFilter() { String[] species = {"All Species", "Dog", "Cat", "Bird", "Rabbit", "Fish", "Hamster"}; - BlackTextArrayAdapter adapter = new BlackTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, species); + WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, species); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.spinnerSpecies.setAdapter(adapter); @@ -196,7 +196,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { storeList = resource.data.getContent(); - SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStore, storeList, + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); } }); diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java index fa22d21d..0041a0b2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SpinnerUtils.java @@ -5,6 +5,7 @@ import android.widget.ArrayAdapter; import android.widget.Spinner; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; +import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import java.util.ArrayList; import java.util.List; @@ -22,6 +23,22 @@ public class SpinnerUtils { public static void populateSpinner(Context context, Spinner spinner, List data, Function nameExtractor, String defaultText, Long preselectedId, Function idExtractor) { + populateSpinnerWithAdapter(context, spinner, data, nameExtractor, defaultText, preselectedId, idExtractor, false); + } + + /** + * Populates a spinner with white text (for dark backgrounds). + */ + public static void populateWhiteSpinner(Context context, Spinner spinner, List data, + Function nameExtractor, String defaultText, + Long preselectedId, Function idExtractor) { + populateSpinnerWithAdapter(context, spinner, data, nameExtractor, defaultText, preselectedId, idExtractor, true); + } + + private static void populateSpinnerWithAdapter(Context context, Spinner spinner, List data, + Function nameExtractor, String defaultText, + Long preselectedId, Function idExtractor, + boolean useWhiteText) { List names = new ArrayList<>(); if (defaultText != null) { names.add(defaultText); @@ -31,8 +48,15 @@ public class SpinnerUtils { names.add(nameExtractor.apply(item)); } - spinner.setAdapter(new BlackTextArrayAdapter<>(context, - android.R.layout.simple_spinner_item, names)); + ArrayAdapter adapter; + if (useWhiteText) { + adapter = new WhiteTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names); + } else { + adapter = new BlackTextArrayAdapter<>(context, android.R.layout.simple_spinner_item, names); + } + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); if (preselectedId != null && preselectedId != -1) { int offset = (defaultText != null) ? 1 : 0; diff --git a/android/app/src/main/res/drawable/bg_search_bar.xml b/android/app/src/main/res/drawable/bg_search_bar.xml new file mode 100644 index 00000000..342e4649 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_search_bar.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/bg_spinner.xml b/android/app/src/main/res/drawable/bg_spinner.xml new file mode 100644 index 00000000..0fcab8e6 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_spinner.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_pet.xml b/android/app/src/main/res/layout/fragment_pet.xml index 2e79ed1b..9273c0f0 100644 --- a/android/app/src/main/res/layout/fragment_pet.xml +++ b/android/app/src/main/res/layout/fragment_pet.xml @@ -54,50 +54,81 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="8dp" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" android:visibility="gone" - android:background="@color/white"> + android:background="@color/primary_dark" + android:elevation="4dp"> - + android:layout_height="44dp" + android:background="@drawable/bg_search_bar" + android:gravity="center_vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp"> + + + + + + + android:orientation="horizontal" + android:layout_marginTop="8dp"> + android:background="@drawable/bg_spinner" + android:paddingStart="12dp" + android:paddingEnd="8dp"/> + + + android:background="@drawable/bg_spinner" + android:paddingStart="12dp" + android:paddingEnd="8dp"/> + android:layout_height="44dp" + android:layout_marginTop="8dp" + android:background="@drawable/bg_spinner" + android:paddingStart="12dp" + android:paddingEnd="8dp"/> From 37bd69c6f154c2fd847751b7669f9d8ca81289b2 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:12:29 -0600 Subject: [PATCH 106/137] updated search for service to call api --- .../petstoremobile/api/ServiceApi.java | 4 +- .../listfragments/ServiceFragment.java | 54 +++++++-------- .../AppointmentDetailFragment.java | 2 +- .../repositories/ServiceRepository.java | 4 +- .../viewmodels/ServiceViewModel.java | 4 +- .../src/main/res/layout/fragment_service.xml | 66 +++++++++++++++---- 6 files changed, 90 insertions(+), 44 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java index a8e4ed32..b659a95f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java @@ -18,7 +18,9 @@ public interface ServiceApi { @GET("api/v1/services") Call> getAllServices( @Query("page") int page, - @Query("size") int size + @Query("size") int size, + @Query("q") String query, + @Query("sort") String sort ); // Get service by id diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java index ff8e1bb5..bda282ce 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -34,7 +34,6 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic private FragmentServiceBinding binding; private List serviceList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); private ServiceAdapter adapter; private ServiceViewModel viewModel; @@ -58,6 +57,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic setupRecyclerView(); setupSearch(); setupSwipeRefresh(); + setupFilterToggle(); loadServiceData(); //Add button to opens the add dialog @@ -83,6 +83,24 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding = null; } + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset search when closing + binding.etSearchService.setText(""); + } + }); + } + /** * Configures the search bar for filtering. */ @@ -90,31 +108,12 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic binding.etSearchService.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - filterServices(s.toString()); + loadServiceData(); } @Override public void afterTextChanged(Editable s) {} }); } - /** - * Filters the service list based on the search query across name and description fields. - */ - private void filterServices(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(serviceList); - } else { - String lower = query.toLowerCase(); - for (ServiceDTO s : serviceList) { - if ((s.getServiceName() != null && s.getServiceName().toLowerCase().contains(lower)) - || (s.getServiceDesc() != null && s.getServiceDesc().toLowerCase().contains(lower))) { - filteredList.add(s); - } - } - } - adapter.notifyDataSetChanged(); - } - /** * Sets up the SwipeRefreshLayout to allow manual reloading of service data. */ @@ -131,7 +130,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic //if editing a service, add the service id to the bundle if (position != -1) { - ServiceDTO service = filteredList.get(position); + ServiceDTO service = serviceList.get(position); args.putLong("serviceId", service.getServiceId()); } @@ -150,8 +149,11 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic * Fetches all service data from the server through the ViewModel and updates the UI. */ private void loadServiceData() { - //Load all services from the backend using viewModel - viewModel.getAllServices(0, 100).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchService != null ? binding.etSearchService.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + //Load services from the backend with query and default sort + viewModel.getAllServices(0, 100, query, "serviceName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -166,7 +168,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic if (resource.data != null) { serviceList.clear(); serviceList.addAll(resource.data.getContent()); - filterServices(binding.etSearchService != null ? binding.etSearchService.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -185,7 +187,7 @@ public class ServiceFragment extends Fragment implements ServiceAdapter.OnServic * Initializes the RecyclerView with a layout manager and adapter for services. */ private void setupRecyclerView() { - adapter = new ServiceAdapter(filteredList, this); + adapter = new ServiceAdapter(serviceList, this); binding.recyclerViewServices.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewServices.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index 3b757b87..d497f529 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -155,7 +155,7 @@ public class AppointmentDetailFragment extends Fragment { * Loads the list of services from the API. */ private void loadServices() { - serviceViewModel.getAllServices(0, 200).observe(getViewLifecycleOwner(), resource -> { + serviceViewModel.getAllServices(0, 200, null, "serviceName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { serviceList = resource.data.getContent(); refreshServiceSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java index eac8bb32..7787c36a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ServiceRepository.java @@ -23,8 +23,8 @@ public class ServiceRepository extends BaseRepository { /** * Retrieves a paginated list of all services from the API. */ - public LiveData>> getAllServices(int page, int size) { - return executeCall(serviceApi.getAllServices(page, size)); + public LiveData>> getAllServices(int page, int size, String query, String sort) { + return executeCall(serviceApi.getAllServices(page, size, query, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java index be4cee20..142ac85b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ServiceViewModel.java @@ -24,8 +24,8 @@ public class ServiceViewModel extends ViewModel { /** * Fetches a paginated list of all services. */ - public LiveData>> getAllServices(int page, int size) { - return repository.getAllServices(page, size); + public LiveData>> getAllServices(int page, int size, String query, String sort) { + return repository.getAllServices(page, size, query, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_service.xml b/android/app/src/main/res/layout/fragment_service.xml index a74f261a..46e7b524 100644 --- a/android/app/src/main/res/layout/fragment_service.xml +++ b/android/app/src/main/res/layout/fragment_service.xml @@ -29,27 +29,69 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> + + + + + + + + + + Date: Tue, 7 Apr 2026 04:19:51 -0600 Subject: [PATCH 107/137] updated search to call api for supplier --- .../petstoremobile/api/SupplierApi.java | 4 +- .../listfragments/SupplierFragment.java | 55 ++++++++-------- .../ProductSupplierDetailFragment.java | 2 +- .../repositories/SupplierRepository.java | 4 +- .../viewmodels/SupplierViewModel.java | 4 +- .../src/main/res/layout/fragment_supplier.xml | 66 +++++++++++++++---- 6 files changed, 90 insertions(+), 45 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java index 47d4e1e3..9f870a51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java @@ -18,7 +18,9 @@ public interface SupplierApi { @GET("api/v1/suppliers") Call> getAllSuppliers( @Query("page") int page, - @Query("size") int size + @Query("size") int size, + @Query("q") String query, + @Query("sort") String sort ); // Get supplier by id diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java index e3a0c131..3d2e038d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -34,7 +34,6 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp private FragmentSupplierBinding binding; private List supplierList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); private SupplierAdapter adapter; private SupplierViewModel viewModel; @@ -58,6 +57,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp setupRecyclerView(); setupSearch(); setupSwipeRefresh(); + setupFilterToggle(); loadSupplierData(); //Add button to opens the add dialog @@ -83,6 +83,24 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp binding = null; } + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset search when closing + binding.etSearchSupplier.setText(""); + } + }); + } + /** * Configures the search bar for filtering. */ @@ -90,32 +108,12 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp binding.etSearchSupplier.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - filterSuppliers(s.toString()); + loadSupplierData(); } @Override public void afterTextChanged(Editable s) {} }); } - /** - * Filters the supplier list based on the search query across company name and contact person. - */ - private void filterSuppliers(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(supplierList); - } else { - String lower = query.toLowerCase(); - for (SupplierDTO s : supplierList) { - if ((s.getSupCompany() != null && s.getSupCompany().toLowerCase().contains(lower)) - || (s.getSupContactFirstName() != null && s.getSupContactFirstName().toLowerCase().contains(lower)) - || (s.getSupContactLastName() != null && s.getSupContactLastName().toLowerCase().contains(lower))) { - filteredList.add(s); - } - } - } - adapter.notifyDataSetChanged(); - } - /** * Sets up the SwipeRefreshLayout to allow manual reloading of supplier data. */ @@ -132,7 +130,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp //if editing a supplier, add the supplier id to the bundle if (position != -1) { - SupplierDTO supplier = filteredList.get(position); + SupplierDTO supplier = supplierList.get(position); args.putLong("supId", supplier.getSupId()); } @@ -152,8 +150,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp * Fetches all supplier data from the server through the ViewModel and updates the UI. */ private void loadSupplierData() { - //Load all suppliers from the backend using viewModel - viewModel.getAllSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + //Load suppliers from the backend with query and default sort + viewModel.getAllSuppliers(0, 100, query, "supCompany").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -168,7 +169,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp if (resource.data != null) { supplierList.clear(); supplierList.addAll(resource.data.getContent()); - filterSuppliers(binding.etSearchSupplier.getText().toString()); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -187,7 +188,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp * Initializes the RecyclerView with a layout manager and adapter for displaying suppliers. */ private void setupRecyclerView() { - adapter = new SupplierAdapter(filteredList, this); + adapter = new SupplierAdapter(supplierList, this); binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewSuppliers.setAdapter(adapter); } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 495c0f6d..0674c458 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -107,7 +107,7 @@ public class ProductSupplierDetailFragment extends Fragment { * Loads the list of suppliers from the API. */ private void loadSuppliers() { - supplierViewModel.getAllSuppliers(0, 200).observe(getViewLifecycleOwner(), resource -> { + supplierViewModel.getAllSuppliers(0, 200, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { supplierList = resource.data.getContent(); refreshSupplierSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java index ec0489ca..e4eb4c0c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/SupplierRepository.java @@ -23,8 +23,8 @@ public class SupplierRepository extends BaseRepository { /** * Retrieves a paginated list of all suppliers from the API. */ - public LiveData>> getAllSuppliers(int page, int size) { - return executeCall(supplierApi.getAllSuppliers(page, size)); + public LiveData>> getAllSuppliers(int page, int size, String query, String sort) { + return executeCall(supplierApi.getAllSuppliers(page, size, query, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java index 7885c898..a89426de 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/SupplierViewModel.java @@ -24,8 +24,8 @@ public class SupplierViewModel extends ViewModel { /** * Fetches a paginated list of all suppliers. */ - public LiveData>> getAllSuppliers(int page, int size) { - return repository.getAllSuppliers(page, size); + public LiveData>> getAllSuppliers(int page, int size, String query, String sort) { + return repository.getAllSuppliers(page, size, query, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_supplier.xml b/android/app/src/main/res/layout/fragment_supplier.xml index 9036b6ea..197367d1 100644 --- a/android/app/src/main/res/layout/fragment_supplier.xml +++ b/android/app/src/main/res/layout/fragment_supplier.xml @@ -29,27 +29,69 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> + + + + + + + + + + Date: Tue, 7 Apr 2026 05:09:48 -0600 Subject: [PATCH 108/137] updated inventory backend to have filter by store and added more search features to andriod --- .../petstoremobile/api/InventoryApi.java | 6 +- .../petstoremobile/api/ProductApi.java | 4 +- .../listfragments/InventoryFragment.java | 160 ++++++---------- .../listfragments/ProductFragment.java | 175 +++++++++++------- .../InventoryDetailFragment.java | 2 +- .../ProductSupplierDetailFragment.java | 2 +- .../repositories/InventoryRepository.java | 6 +- .../repositories/ProductRepository.java | 6 +- .../viewmodels/InventoryViewModel.java | 17 +- .../viewmodels/ProductViewModel.java | 6 +- .../main/res/layout/fragment_inventory.xml | 82 +++++--- .../src/main/res/layout/fragment_product.xml | 76 ++++++-- .../controller/InventoryController.java | 3 +- .../repository/InventoryRepository.java | 6 +- .../backend/service/InventoryService.java | 18 +- 15 files changed, 342 insertions(+), 227 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java index f54616ee..6c747e6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/InventoryApi.java @@ -16,12 +16,14 @@ import retrofit2.http.Query; public interface InventoryApi { - // GET /api/v1/inventory?q=...&page=...&size=... + // GET /api/v1/inventory?q=...&page=...&size=...&category=...&storeId=...&sort=... @GET("api/v1/inventory") Call> getAllInventory( - @Query("q") String query, @Query("page") int page, @Query("size") int size, + @Query("q") String query, + @Query("category") String category, + @Query("storeId") Long storeId, @Query("sort") String sort); // GET /api/v1/inventory/{id} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index dc02fd6c..1d46107b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -12,8 +12,10 @@ public interface ProductApi { @GET("api/v1/products") Call> getAllProducts( @Query("q") String query, + @Query("categoryId") Long categoryId, @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("sort") String sort); @GET("api/v1/products/{id}") Call getProductById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 029dc446..1fe9d188 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -1,8 +1,6 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; @@ -21,14 +19,14 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.adapters.InventoryAdapter; import com.example.petstoremobile.databinding.FragmentInventoryBinding; -import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import java.util.ArrayList; import java.util.List; @@ -43,26 +41,15 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private FragmentInventoryBinding binding; private final List inventoryList = new ArrayList<>(); - private final List categoryList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private InventoryAdapter adapter; private InventoryViewModel viewModel; - // Debounce search - private final Handler searchHandler = new Handler(Looper.getMainLooper()); - private Runnable searchRunnable; - private String currentQuery = ""; - - // Selected category filter — null means "All" - private String selectedCategory = null; - // Pagination private int currentPage = 0; private boolean isLastPage = false; private boolean isLoading = false; - // Prevent spinner from firing on initial load - private boolean spinnerReady = false; - /** * Initializes the fragment and its ViewModel. */ @@ -73,7 +60,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } /** - * Sets up the fragment's UI components, including the inventory list, search, and category filter. + * Sets up the fragment's UI components, including the inventory list and search. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -82,9 +69,11 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupRecyclerView(); setupSearch(); + setupStoreFilter(); setupSwipeRefresh(); - loadCategories(); // loads categories then triggers loadInventory + setupFilterToggle(); loadInventory(true); + loadStoreData(); binding.fabAddInventory.setOnClickListener(v -> openDetail(null)); @@ -110,83 +99,59 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } /** - * Fetches all product categories to populate the filter spinner. + * Sets up the filter toggle button to show/hide the filter layout. */ - private void loadCategories() { - viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { - categoryList.clear(); - categoryList.addAll(resource.data.getContent()); - setupCategorySpinner(); - } else if (resource != null && resource.status == Resource.Status.ERROR) { - Log.e(TAG, "Failed to load categories: " + resource.message); - setupCategorySpinner(); + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchInventory.setText(""); + binding.spinnerStore.setSelection(0); } }); } - /** - * Setup the category filter spinner. - */ - private void setupCategorySpinner() { - // First item is always "All Categories" - List categoryNames = new ArrayList<>(); - categoryNames.add("All Categories"); - for (CategoryDTO c : categoryList) { - categoryNames.add(c.getCategoryName()); - } - - if (getContext() != null) { - BlackTextArrayAdapter spinnerAdapter = new BlackTextArrayAdapter<>( - requireContext(), - android.R.layout.simple_spinner_item, - categoryNames); - spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.spinnerCategory.setAdapter(spinnerAdapter); - - binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (!spinnerReady) { - // Skip the first automatic trigger on setup - spinnerReady = true; - return; - } - if (position == 0) { - selectedCategory = null; // "All Categories" - } else { - selectedCategory = categoryList.get(position - 1).getCategoryName(); - } - loadInventory(true); - } - - @Override - public void onNothingSelected(AdapterView parent) { - } - }); - } - } - /** * Sets up the search bar for filtering. */ private void setupSearch() { binding.etSearchInventory.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { - } - - @Override public void afterTextChanged(Editable s) { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadInventory(true); } + @Override public void afterTextChanged(Editable s) {} + }); + } + /** + * Configures the store filter spinner. + */ + private void setupStoreFilter() { + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (searchRunnable != null) - searchHandler.removeCallbacks(searchRunnable); - searchRunnable = () -> { - currentQuery = s.toString().trim(); - loadInventory(true); - }; - searchHandler.postDelayed(searchRunnable, 400); + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadInventory(true); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Fetches store data to populate the store filter. + */ + private void loadStoreData() { + viewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); } }); } @@ -228,19 +193,24 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn * Fetches a page of inventory items from the API. */ private void loadInventory(boolean reset) { - if (isLoading) - return; + if (isLoading) return; if (reset) { currentPage = 0; isLastPage = false; } - // Build query: combine search text + selected category - String q = buildQuery(); + // Search text from input + String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + Long storeId = null; + if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { + storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } //Load all inventory items from the backend using viewModel - viewModel.getAllInventory(q, currentPage, PAGE_SIZE, "inventoryId,asc").observe(getViewLifecycleOwner(), resource -> { + viewModel.getAllInventory(query, null, storeId, currentPage, PAGE_SIZE, "product.prodName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -273,22 +243,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - /** - * Constructs a query string based on the current search text and selected category. - */ - private String buildQuery() { - String q = null; - if (!currentQuery.isEmpty() && selectedCategory != null) { - // Both active — prioritize search text, category acts as context - q = currentQuery; - } else if (!currentQuery.isEmpty()) { - q = currentQuery; - } else if (selectedCategory != null) { - q = selectedCategory; - } - return q; - } - /** * Displays a confirmation dialog before performing a bulk deletion of selected items. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 6103918e..0eea959f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -1,10 +1,7 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.*; -import android.util.Log; -import android.view.*; -import android.widget.*; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -12,15 +9,27 @@ import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductAdapter; -import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentProductBinding; +import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.ProductViewModel; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -32,12 +41,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc private FragmentProductBinding binding; private List productList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List categoryList = new ArrayList<>(); private ProductAdapter adapter; private ProductViewModel viewModel; @Inject @Named("baseUrl") String baseUrl; - @Inject TokenManager tokenManager; /** * Initializes the fragment and its associated ProductViewModel. @@ -49,7 +57,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Sets up the fragment's UI components, including the product list, search, and swipe-to-refresh. + * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -58,11 +66,11 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupRecyclerView(); setupSearch(); + setupCategoryFilter(); setupSwipeRefresh(); + setupFilterToggle(); - loadProducts(); - - binding.fabAddProduct.setOnClickListener(v -> openDetail(-1)); + binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); binding.btnHamburgerProduct.setOnClickListener(v -> { Fragment parent = getParentFragment(); @@ -84,61 +92,110 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Initializes the RecyclerView with a layout manager and adapter for displaying products. + * Reloads data every time the fragment becomes visible. */ - private void setupRecyclerView() { - adapter = new ProductAdapter(filteredList, this); - adapter.setBaseUrl(baseUrl); - adapter.setToken(tokenManager.getToken()); - binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewProducts.setAdapter(adapter); + @Override + public void onResume() { + super.onResume(); + loadProductData(); + loadCategoryData(); } /** - * Configures the search bar for filtering. + * Sets up the filter toggle button to show/hide the filter layout. */ - private void setupSearch() { - binding.etSearchProduct.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(Editable s) {} - public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(); + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchProduct.setText(""); + binding.spinnerCategory.setSelection(0); } }); } /** - * Sets up the SwipeRefreshLayout to allow manual re-fetching of product data. + * Configures the search bar for triggering data load from backend. + */ + private void setupSearch() { + binding.etSearchProduct.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadProductData(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + /** + * Configures the category filter spinner. + */ + private void setupCategoryFilter() { + binding.spinnerCategory.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadProductData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Fetches category data to populate the category filter. + */ + private void loadCategoryData() { + viewModel.getAllCategories(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + categoryList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, categoryList, + CategoryDTO::getCategoryName, "All Categories", -1L, CategoryDTO::getCategoryId); + } + }); + } + + /** + * Sets up the SwipeRefreshLayout. */ private void setupSwipeRefresh() { - binding.swipeRefreshProduct.setOnRefreshListener(this::loadProducts); + binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData); } /** - * Filters the product list based on the search query across name, category, and description. + * Navigates to the product detail screen. */ - private void filter() { - String query = binding.etSearchProduct.getText().toString().toLowerCase(); - - filteredList.clear(); - for (ProductDTO p : productList) { - boolean matchesSearch = query.isEmpty() || - (p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) || - (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) || - (p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query)); - - if (matchesSearch) { - filteredList.add(p); - } + private void openProductDetails(int position) { + Bundle args = new Bundle(); + if (position != -1) { + ProductDTO product = productList.get(position); + args.putLong("productId", product.getProdId()); } - adapter.notifyDataSetChanged(); + NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); + } + + @Override + public void onProductClick(int position) { + openProductDetails(position); } /** - * Fetches all product data from the server through the ViewModel and updates the UI. + * Fetches product data from the server with search query, category, and sorting. */ - private void loadProducts() { - viewModel.getAllProducts(null, 0, 100).observe(getViewLifecycleOwner(), resource -> { + private void loadProductData() { + String query = binding.etSearchProduct.getText().toString().trim(); + if (query.isEmpty()) query = null; + + Long categoryId = null; + if (binding.spinnerCategory.getSelectedItemPosition() > 0 && !categoryList.isEmpty()) { + categoryId = categoryList.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getCategoryId(); + } + + viewModel.getAllProducts(query, categoryId, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; switch (resource.status) { @@ -150,12 +207,14 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (resource.data != null) { productList.clear(); productList.addAll(resource.data.getContent()); - filter(); + adapter.notifyDataSetChanged(); } break; case ERROR: binding.swipeRefreshProduct.setRefreshing(false); - Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to load products: " + resource.message, Toast.LENGTH_SHORT).show(); + } Log.e("ProductFragment", "Error loading products: " + resource.message); break; } @@ -163,20 +222,12 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc } /** - * Navigates to the product detail screen for a specific product or to add a new one. + * Initializes the RecyclerView. */ - private void openDetail(int position) { - Bundle args = new Bundle(); - if (position != -1) { - ProductDTO p = filteredList.get(position); - args.putLong("prodId", p.getProdId()); - } - NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args); + private void setupRecyclerView() { + adapter = new ProductAdapter(productList, this); + adapter.setBaseUrl(baseUrl); + binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewProducts.setAdapter(adapter); } - - /** - * Handles item click in the product list. - */ - @Override - public void onProductClick(int position) { openDetail(position); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index cd1e84f4..1a32df6e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -154,7 +154,7 @@ public class InventoryDetailFragment extends Fragment { */ private void searchProducts(String query) { if (getView() == null) return; - productViewModel.getAllProducts(query, 0, 20).observe(getViewLifecycleOwner(), resource -> { + productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productSuggestions.clear(); productSuggestions.addAll(resource.data.getContent()); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java index 0674c458..aa570d13 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductSupplierDetailFragment.java @@ -89,7 +89,7 @@ public class ProductSupplierDetailFragment extends Fragment { * Loads the list of products from the API. */ private void loadProducts() { - productViewModel.getAllProducts(null, 0, 200).observe(getViewLifecycleOwner(), resource -> { + productViewModel.getAllProducts(null, null, 0, 200, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { productList = resource.data.getContent(); refreshProductSpinner(); diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 5513d0e3..05719c38 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -23,10 +23,10 @@ public class InventoryRepository extends BaseRepository { } /** - * Retrieves a paginated list of inventory items from the API with optional search and sort. + * Retrieves a paginated list of inventory items from the API with optional search, category, storeId and sort. */ - public LiveData>> getAllInventory(String query, int page, int size, String sort) { - return executeCall(inventoryApi.getAllInventory(query, page, size, sort)); + public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { + return executeCall(inventoryApi.getAllInventory(page, size, query, category, storeId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java index 5ed95c8a..a6d32336 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductRepository.java @@ -23,10 +23,10 @@ public class ProductRepository extends BaseRepository { } /** - * Retrieves a paginated list of products from the API, filtered by an optional query. + * Retrieves a paginated list of products from the API, filtered by an optional query, category and sorted. */ - public LiveData>> getAllProducts(String query, int page, int size) { - return executeCall(productApi.getAllProducts(query, page, size)); + public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { + return executeCall(productApi.getAllProducts(query, categoryId, page, size, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java index 3af31b5c..02a5f1bb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -8,8 +8,10 @@ import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.CategoryRepository; import com.example.petstoremobile.repositories.InventoryRepository; +import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; import java.util.List; @@ -22,18 +24,20 @@ import dagger.hilt.android.lifecycle.HiltViewModel; public class InventoryViewModel extends ViewModel { private final InventoryRepository inventoryRepository; private final CategoryRepository categoryRepository; + private final StoreRepository storeRepository; @Inject - public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository) { + public InventoryViewModel(InventoryRepository inventoryRepository, CategoryRepository categoryRepository, StoreRepository storeRepository) { this.inventoryRepository = inventoryRepository; this.categoryRepository = categoryRepository; + this.storeRepository = storeRepository; } /** * Retrieves a paginated list of inventory items, with optional filtering and sorting. */ - public LiveData>> getAllInventory(String query, int page, int size, String sort) { - return inventoryRepository.getAllInventory(query, page, size, sort); + public LiveData>> getAllInventory(String query, String category, Long storeId, int page, int size, String sort) { + return inventoryRepository.getAllInventory(query, category, storeId, page, size, sort); } /** @@ -77,4 +81,11 @@ public class InventoryViewModel extends ViewModel { public LiveData>> getAllCategories(int page, int size) { return categoryRepository.getAllCategories(page, size); } + + /** + * Retrieves a paginated list of stores. + */ + public LiveData>> getAllStores(int page, int size) { + return storeRepository.getAllStores(page, size); + } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java index 6edcdd4b..b44c08eb 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductViewModel.java @@ -27,10 +27,10 @@ public class ProductViewModel extends ViewModel { } /** - * Retrieves a paginated list of products, optionally filtered by a query string. + * Retrieves a paginated list of products, optionally filtered by a query string, category and sorted. */ - public LiveData>> getAllProducts(String query, int page, int size) { - return productRepository.getAllProducts(query, page, size); + public LiveData>> getAllProducts(String query, Long categoryId, int page, int size, String sort) { + return productRepository.getAllProducts(query, categoryId, page, size, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_inventory.xml b/android/app/src/main/res/layout/fragment_inventory.xml index 4b075953..8197078e 100644 --- a/android/app/src/main/res/layout/fragment_inventory.xml +++ b/android/app/src/main/res/layout/fragment_inventory.xml @@ -30,42 +30,78 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> - + + + + + + + + android:id="@+id/spinnerStore" + android:layout_width="match_parent" + android:layout_height="44dp" + android:layout_marginTop="8dp" + android:background="@drawable/bg_spinner" + android:paddingStart="12dp" + android:paddingEnd="8dp"/> + diff --git a/android/app/src/main/res/layout/fragment_product.xml b/android/app/src/main/res/layout/fragment_product.xml index 21c9d9ec..8dba3f51 100644 --- a/android/app/src/main/res/layout/fragment_product.xml +++ b/android/app/src/main/res/layout/fragment_product.xml @@ -12,6 +12,7 @@ android:orientation="vertical"> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> + + + + + + + + + + + + > getAllInventory( @RequestParam(required = false) String q, + @RequestParam(required = false) Long storeId, Pageable pageable) { - return ResponseEntity.ok(inventoryService.getAllInventory(q, pageable)); + return ResponseEntity.ok(inventoryService.getAllInventory(q, storeId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java index b448b497..7dd535eb 100644 --- a/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/InventoryRepository.java @@ -20,8 +20,10 @@ public interface InventoryRepository extends JpaRepository { Optional findByProductIdAndStoreId(@Param("productId") Long productId, @Param("storeId") Long storeId); @Query("SELECT i FROM Inventory i LEFT JOIN i.store s WHERE " + + "(:q IS NULL OR (" + "LOWER(i.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(i.product.category.categoryName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchInventory(@Param("q") String query, Pageable pageable); + "LOWER(s.storeName) LIKE LOWER(CONCAT('%', :q, '%')))) AND " + + "(:storeId IS NULL OR i.store.storeId = :storeId)") + Page searchInventory(@Param("q") String query, @Param("storeId") Long storeId, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index 499e8dd5..884458f9 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -28,13 +28,9 @@ public class InventoryService { this.storeRepository = storeRepository; } - public Page getAllInventory(String query, Pageable pageable) { - Page inventory; - if (query != null && !query.trim().isEmpty()) { - inventory = inventoryRepository.searchInventory(query, pageable); - } else { - inventory = inventoryRepository.findAll(pageable); - } + public Page getAllInventory(String query, Long storeId, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + Page inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable); return inventory.map(this::mapToResponse); } @@ -97,6 +93,14 @@ public class InventoryService { inventoryRepository.deleteAllById(request.getIds()); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private InventoryResponse mapToResponse(Inventory inventory) { StoreLocation store = inventory.getStore(); return new InventoryResponse( From 1990022c1e3b721a8d5384cf69254afc6be6ec29 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:24:25 -0600 Subject: [PATCH 109/137] Added filter by store for inventory in back end and added search to inventory --- .../petstoremobile/api/PurchaseOrderApi.java | 5 +- .../listfragments/PurchaseOrderFragment.java | 134 ++++++++++++------ .../repositories/PurchaseOrderRepository.java | 4 +- .../viewmodels/PurchaseOrderViewModel.java | 4 +- .../res/layout/fragment_purchase_order.xml | 76 ++++++++-- .../controller/PurchaseOrderController.java | 3 +- .../repository/PurchaseOrderRepository.java | 5 +- .../backend/service/PurchaseOrderService.java | 18 ++- 8 files changed, 181 insertions(+), 68 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java index 1e4f4ffe..e5a5a06d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PurchaseOrderApi.java @@ -12,7 +12,10 @@ public interface PurchaseOrderApi { @GET("api/v1/purchase-orders") Call> getAllPurchaseOrders( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("query") String query, + @Query("storeId") Long storeId, + @Query("sort") String sort); @GET("api/v1/purchase-orders/{id}") Call getPurchaseOrderById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java index 78fcd981..452f8d69 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PurchaseOrderFragment.java @@ -1,10 +1,15 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.*; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -16,10 +21,15 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.PurchaseOrderAdapter; import com.example.petstoremobile.databinding.FragmentPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.PurchaseOrderViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @@ -29,21 +39,23 @@ public class PurchaseOrderFragment extends Fragment private FragmentPurchaseOrderBinding binding; private List poList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private PurchaseOrderAdapter adapter; private PurchaseOrderViewModel viewModel; + private StoreViewModel storeViewModel; /** - * Initializes the fragment and its associated PurchaseOrderViewModel. + * Initializes the fragment and its associated ViewModels. */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PurchaseOrderViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** - * Sets up the fragment's UI components, including RecyclerView, search, and swipe-to-refresh. + * Sets up the fragment's UI components, including RecyclerView, filters, and swipe-to-refresh. */ @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -52,8 +64,9 @@ public class PurchaseOrderFragment extends Fragment setupRecyclerView(); setupSearch(); + setupStoreFilter(); setupSwipeRefresh(); - loadData(); + setupFilterToggle(); binding.btnHamburgerPO.setOnClickListener(v -> { Fragment parent = getParentFragment(); @@ -75,12 +88,32 @@ public class PurchaseOrderFragment extends Fragment } /** - * Initializes the RecyclerView with a layout manager and adapter for purchase orders. + * Reloads data every time the fragment becomes visible. */ - private void setupRecyclerView() { - adapter = new PurchaseOrderAdapter(filteredList, this); - binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); - binding.recyclerViewPO.setAdapter(adapter); + @Override + public void onResume() { + super.onResume(); + loadData(); + loadStoreData(); + } + + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchPO.setText(""); + binding.spinnerStore.setSelection(0); + } + }); } /** @@ -88,18 +121,49 @@ public class PurchaseOrderFragment extends Fragment */ private void setupSearch() { binding.etSearchPO.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int a, int b, int c) { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadData(); } + @Override public void afterTextChanged(Editable s) {} + }); + } - public void afterTextChanged(Editable s) { + /** + * Configures the store filter spinner. + */ + private void setupStoreFilter() { + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadData(); } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } - public void onTextChanged(CharSequence s, int start, int before, int count) { - filter(s.toString()); + /** + * Fetches store data to populate the store filter. + */ + private void loadStoreData() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); } }); } + /** + * Initializes the RecyclerView with a layout manager and adapter. + */ + private void setupRecyclerView() { + adapter = new PurchaseOrderAdapter(poList, this); + binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerViewPO.setAdapter(adapter); + } + /** * Sets up the SwipeRefreshLayout to allow manual reloading of purchase order data. */ @@ -108,30 +172,18 @@ public class PurchaseOrderFragment extends Fragment } /** - * Filters the purchase order list based on the search query. - */ - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(poList); - } else { - String lower = query.toLowerCase(); - for (PurchaseOrderDTO po : poList) { - if ((po.getSupplierName() != null && po.getSupplierName().toLowerCase().contains(lower)) - || (po.getStatus() != null && po.getStatus().toLowerCase().contains(lower))) { - filteredList.add(po); - } - } - } - adapter.notifyDataSetChanged(); - } - - /** - * Fetches all purchase order data from the server through the ViewModel and updates the UI. + * Fetches purchase order data from the server with active filters and updates the UI. */ private void loadData() { - //Load all purchase orders from the backend using viewModel - viewModel.getAllPurchaseOrders(0, 100).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : ""; + if (query.isEmpty()) query = null; + + Long storeId = null; + if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { + storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + viewModel.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -146,7 +198,7 @@ public class PurchaseOrderFragment extends Fragment if (resource.data != null) { poList.clear(); poList.addAll(resource.data.getContent()); - filter(binding.etSearchPO != null ? binding.etSearchPO.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -164,7 +216,7 @@ public class PurchaseOrderFragment extends Fragment */ private void openDetail(int position) { Bundle args = new Bundle(); - PurchaseOrderDTO po = filteredList.get(position); + PurchaseOrderDTO po = poList.get(position); args.putLong("purchaseOrderId", po.getPurchaseOrderId()); NavHostFragment.findNavController(this).navigate(R.id.nav_purchase_order_detail, args); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java index bd1b224e..b55f9a33 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PurchaseOrderRepository.java @@ -23,8 +23,8 @@ public class PurchaseOrderRepository extends BaseRepository { /** * Retrieves a paginated list of all purchase orders from the API. */ - public LiveData>> getAllPurchaseOrders(int page, int size) { - return executeCall(api.getAllPurchaseOrders(page, size)); + public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { + return executeCall(api.getAllPurchaseOrders(page, size, query, storeId, sort)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java index 0b5d7ea1..d9a24e5e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PurchaseOrderViewModel.java @@ -24,8 +24,8 @@ public class PurchaseOrderViewModel extends ViewModel { /** * Fetches a paginated list of all purchase orders. */ - public LiveData>> getAllPurchaseOrders(int page, int size) { - return repository.getAllPurchaseOrders(page, size); + public LiveData>> getAllPurchaseOrders(int page, int size, String query, Long storeId, String sort) { + return repository.getAllPurchaseOrders(page, size, query, storeId, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_purchase_order.xml b/android/app/src/main/res/layout/fragment_purchase_order.xml index ea11397d..3ed22d3d 100644 --- a/android/app/src/main/res/layout/fragment_purchase_order.xml +++ b/android/app/src/main/res/layout/fragment_purchase_order.xml @@ -1,6 +1,7 @@ @@ -27,27 +28,78 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> + + + + + + + + + + + + > getAllPurchaseOrders( @RequestParam(required = false) String q, + @RequestParam(required = false) Long storeId, Pageable pageable) { - return ResponseEntity.ok(purchaseOrderService.getAllPurchaseOrders(q, pageable)); + return ResponseEntity.ok(purchaseOrderService.getAllPurchaseOrders(q, storeId, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java b/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java index d3b445c4..5ebf9457 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PurchaseOrderRepository.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Repository; public interface PurchaseOrderRepository extends JpaRepository { @Query("SELECT po FROM PurchaseOrder po WHERE " + - "LOWER(po.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchPurchaseOrders(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR LOWER(po.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + "(:storeId IS NULL OR po.store.storeId = :storeId)") + Page searchPurchaseOrders(@Param("q") String query, @Param("storeId") Long storeId, Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java index e9f7e4a5..7c42cee9 100644 --- a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -18,13 +18,9 @@ public class PurchaseOrderService { this.purchaseOrderRepository = purchaseOrderRepository; } - public Page getAllPurchaseOrders(String query, Pageable pageable) { - Page purchaseOrders; - if (query != null && !query.trim().isEmpty()) { - purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(query, pageable); - } else { - purchaseOrders = purchaseOrderRepository.findAll(pageable); - } + public Page getAllPurchaseOrders(String query, Long storeId, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + Page purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(normalizedQuery, storeId, pageable); return purchaseOrders.map(this::mapToResponse); } @@ -34,6 +30,14 @@ public class PurchaseOrderService { return mapToResponse(purchaseOrder); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) { StoreLocation store = purchaseOrder.getStore(); return new PurchaseOrderResponse( From 195c4605f064484cf84e0f4a90a0f2dbc5fef6a6 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:48:24 -0600 Subject: [PATCH 110/137] changed backend so can sortBy productName and added search to productSupplier --- .../api/ProductSupplierApi.java | 6 +- .../fragments/listfragments/PetFragment.java | 2 +- .../ProductSupplierFragment.java | 162 ++++++++++++++---- .../ProductSupplierRepository.java | 6 +- .../viewmodels/ProductSupplierViewModel.java | 4 +- .../res/layout/fragment_product_supplier.xml | 97 +++++++++-- .../controller/ProductSupplierController.java | 4 +- .../repository/ProductSupplierRepository.java | 12 +- .../service/ProductSupplierService.java | 43 ++++- 9 files changed, 270 insertions(+), 66 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java index 7ec9eb07..67a0e7f2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductSupplierApi.java @@ -10,7 +10,11 @@ public interface ProductSupplierApi { @GET("api/v1/product-suppliers") Call> getAllProductSuppliers( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("productId") Long productId, + @Query("supplierId") Long supplierId, + @Query("sort") String sort); @GET("api/v1/product-suppliers/{productId}/{supplierId}") Call getProductSupplierById( diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index bcb19fa8..f6b46b0e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -144,7 +144,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen * Configures the status filter spinner. */ private void setupStatusFilter() { - String[] statuses = {"All Statuses", "Available", "Adopted"}; + String[] statuses = {"All Statuses", "Available", "Adopted", "Owned"}; WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.spinnerStatus.setAdapter(adapter); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java index 4c8e08d0..1c0a9c19 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductSupplierFragment.java @@ -1,10 +1,15 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; -import android.text.*; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; -import android.view.*; -import android.widget.*; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; @@ -15,11 +20,18 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.ProductSupplierAdapter; import com.example.petstoremobile.databinding.FragmentProductSupplierBinding; +import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.dtos.ProductSupplierDTO; +import com.example.petstoremobile.dtos.SupplierDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.ProductSupplierViewModel; +import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.SupplierViewModel; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import dagger.hilt.android.AndroidEntryPoint; @@ -29,17 +41,23 @@ public class ProductSupplierFragment extends Fragment private FragmentProductSupplierBinding binding; private List psList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List productList = new ArrayList<>(); + private List supplierList = new ArrayList<>(); + private ProductSupplierAdapter adapter; private ProductSupplierViewModel viewModel; + private ProductViewModel productViewModel; + private SupplierViewModel supplierViewModel; /** - * Initializes the fragment and its associated ProductSupplierViewModel. + * Initializes the fragment and its associated ViewModels. */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ProductSupplierViewModel.class); + productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + supplierViewModel = new ViewModelProvider(this).get(SupplierViewModel.class); } /** @@ -52,8 +70,10 @@ public class ProductSupplierFragment extends Fragment setupRecyclerView(); setupSearch(); + setupProductFilter(); + setupSupplierFilter(); setupSwipeRefresh(); - loadData(); + setupFilterToggle(); binding.fabAddPS.setOnClickListener(v -> openDetail(-1)); @@ -76,11 +96,41 @@ public class ProductSupplierFragment extends Fragment binding = null; } + /** + * Reloads data every time the fragment becomes visible. + */ + @Override + public void onResume() { + super.onResume(); + loadData(); + loadFilterData(); + } + + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchPS.setText(""); + binding.spinnerProduct.setSelection(0); + binding.spinnerSupplier.setSelection(0); + } + }); + } + /** * Initializes the RecyclerView with a layout manager and adapter for product-supplier data. */ private void setupRecyclerView() { - adapter = new ProductSupplierAdapter(filteredList, this); + adapter = new ProductSupplierAdapter(psList, this); binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewPS.setAdapter(adapter); } @@ -90,10 +140,57 @@ public class ProductSupplierFragment extends Fragment */ private void setupSearch() { binding.etSearchPS.addTextChangedListener(new TextWatcher() { - public void beforeTextChanged(CharSequence s, int a, int b, int c) {} - public void afterTextChanged(Editable s) {} - public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(s.toString()); + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadData(); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + /** + * Configures the product filter spinner. + */ + private void setupProductFilter() { + binding.spinnerProduct.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Configures the supplier filter spinner. + */ + private void setupSupplierFilter() { + binding.spinnerSupplier.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Fetches products and suppliers to populate the filters. + */ + private void loadFilterData() { + productViewModel.getAllProducts(null, null, 0, 100, "prodName").observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + productList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, productList, + ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId); + } + }); + + supplierViewModel.getAllSuppliers(0, 100, null, "supCompany").observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + supplierList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, supplierList, + SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId); } }); } @@ -106,30 +203,23 @@ public class ProductSupplierFragment extends Fragment } /** - * Filters the product-supplier list based on the search query. - */ - private void filter(String query) { - filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(psList); - } else { - String lower = query.toLowerCase(); - for (ProductSupplierDTO ps : psList) { - if ((ps.getProductName() != null && ps.getProductName().toLowerCase().contains(lower)) - || (ps.getSupplierName() != null && ps.getSupplierName().toLowerCase().contains(lower))) { - filteredList.add(ps); - } - } - } - adapter.notifyDataSetChanged(); - } - - /** - * Fetches all product-supplier data from the server through the ViewModel. + * Fetches product-supplier data from the server through the ViewModel with search query and filters. */ private void loadData() { - //Load all product suppliers from the backend using viewModel - viewModel.getAllProductSuppliers(0, 100).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchPS.getText().toString().trim(); + if (query.isEmpty()) query = null; + + Long productId = null; + if (binding.spinnerProduct.getSelectedItemPosition() > 0 && !productList.isEmpty()) { + productId = productList.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId(); + } + + Long supplierId = null; + if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && !supplierList.isEmpty()) { + supplierId = supplierList.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId(); + } + + viewModel.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName").observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -144,7 +234,7 @@ public class ProductSupplierFragment extends Fragment if (resource.data != null) { psList.clear(); psList.addAll(resource.data.getContent()); - filter(binding.etSearchPS != null ? binding.etSearchPS.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -163,7 +253,7 @@ public class ProductSupplierFragment extends Fragment private void openDetail(int position) { Bundle args = new Bundle(); if (position != -1) { - ProductSupplierDTO ps = filteredList.get(position); + ProductSupplierDTO ps = psList.get(position); args.putLong("productId", ps.getProductId()); args.putLong("supplierId", ps.getSupplierId()); } diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java index 72182918..e5c135a5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ProductSupplierRepository.java @@ -23,8 +23,8 @@ public class ProductSupplierRepository extends BaseRepository { /** * Retrieves a paginated list of all product-supplier relationships from the API. */ - public LiveData>> getAllProductSuppliers(int page, int size) { - return executeCall(api.getAllProductSuppliers(page, size)); + public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { + return executeCall(api.getAllProductSuppliers(page, size, query, productId, supplierId, sort)); } /** @@ -54,4 +54,4 @@ public class ProductSupplierRepository extends BaseRepository { public LiveData> deleteProductSupplier(Long productId, Long supplierId) { return executeCall(api.deleteProductSupplier(productId, supplierId)); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java index dbc55534..95613929 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ProductSupplierViewModel.java @@ -24,8 +24,8 @@ public class ProductSupplierViewModel extends ViewModel { /** * Fetches a paginated list of all product-supplier relationships. */ - public LiveData>> getAllProductSuppliers(int page, int size) { - return repository.getAllProductSuppliers(page, size); + public LiveData>> getAllProductSuppliers(int page, int size, String query, Long productId, Long supplierId, String sort) { + return repository.getAllProductSuppliers(page, size, query, productId, supplierId, sort); } /** diff --git a/android/app/src/main/res/layout/fragment_product_supplier.xml b/android/app/src/main/res/layout/fragment_product_supplier.xml index 6924c36d..86bb7530 100644 --- a/android/app/src/main/res/layout/fragment_product_supplier.xml +++ b/android/app/src/main/res/layout/fragment_product_supplier.xml @@ -28,27 +28,100 @@ android:contentDescription="Open menu"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + - + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:visibility="gone" + android:background="@color/primary_dark" + android:elevation="4dp"> + + + + + + + + + + + + + + + + + + + + > getAllProductSuppliers( @RequestParam(required = false) String q, + @RequestParam(required = false) Long productId, + @RequestParam(required = false) Long supplierId, Pageable pageable) { - return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, pageable)); + return ResponseEntity.ok(productSupplierService.getAllProductSuppliers(q, productId, supplierId, pageable)); } @GetMapping("/{productId}/{supplierId}") diff --git a/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java index 46e87945..15d17703 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ProductSupplierRepository.java @@ -12,7 +12,13 @@ import org.springframework.stereotype.Repository; public interface ProductSupplierRepository extends JpaRepository { @Query("SELECT ps FROM ProductSupplier ps WHERE " + - "LOWER(ps.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(ps.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchProductSuppliers(@Param("q") String query, Pageable pageable); + "(:q IS NULL OR (LOWER(ps.product.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + + "LOWER(ps.supplier.supCompany) LIKE LOWER(CONCAT('%', :q, '%')))) AND " + + "(:productId IS NULL OR ps.product.prodId = :productId) AND " + + "(:supplierId IS NULL OR ps.supplier.supId = :supplierId)") + Page searchProductSuppliers( + @Param("q") String query, + @Param("productId") Long productId, + @Param("supplierId") Long supplierId, + Pageable pageable); } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java index 7e3677a9..c9be85f1 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -11,10 +11,15 @@ import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.ProductSupplierRepository; import com.petshop.backend.repository.SupplierRepository; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + @Service public class ProductSupplierService { @@ -28,13 +33,10 @@ public class ProductSupplierService { this.supplierRepository = supplierRepository; } - public Page getAllProductSuppliers(String query, Pageable pageable) { - Page productSuppliers; - if (query != null && !query.trim().isEmpty()) { - productSuppliers = productSupplierRepository.searchProductSuppliers(query, pageable); - } else { - productSuppliers = productSupplierRepository.findAll(pageable); - } + public Page getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + Pageable mappedPageable = mapSortProperties(pageable); + Page productSuppliers = productSupplierRepository.searchProductSuppliers(normalizedQuery, productId, supplierId, mappedPageable); return productSuppliers.map(this::mapToResponse); } @@ -95,6 +97,33 @@ public class ProductSupplierService { }); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + private Pageable mapSortProperties(Pageable pageable) { + if (pageable.getSort().isUnsorted()) { + return pageable; + } + + List orders = new ArrayList<>(); + for (Sort.Order order : pageable.getSort()) { + String property = order.getProperty(); + if ("productName".equalsIgnoreCase(property)) { + orders.add(new Sort.Order(order.getDirection(), "product.prodName")); + } else if ("supplierName".equalsIgnoreCase(property)) { + orders.add(new Sort.Order(order.getDirection(), "supplier.supCompany")); + } else { + orders.add(order); + } + } + return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders)); + } + private ProductSupplierResponse mapToResponse(ProductSupplier productSupplier) { return new ProductSupplierResponse( productSupplier.getProduct().getProdId(), From 9bab45f04b9f29c9d7daa8bbf52596193ea69df0 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:14:17 -0600 Subject: [PATCH 111/137] updated Appointments on andriod for new backend --- .../adapters/AppointmentAdapter.java | 8 ++--- .../petstoremobile/dtos/AppointmentDTO.java | 33 +++++++------------ .../AppointmentDetailFragment.java | 23 ++++++++----- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java index 9960b5b6..fb260541 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java @@ -59,14 +59,14 @@ public class AppointmentAdapter extends RecyclerView.Adapter petNames; - private List petIds; + private String petName; + private Long petId; private String createdAt; private String updatedAt; public AppointmentDTO(Long customerId, Long storeId, Long serviceId, String appointmentDate, String appointmentTime, - String appointmentStatus, List petIds) { - this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petIds); + String appointmentStatus, Long petId) { + this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petId); } public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId, String appointmentDate, String appointmentTime, - String appointmentStatus, List petIds) { + String appointmentStatus, Long petId) { this.customerId = customerId; this.storeId = storeId; this.serviceId = serviceId; @@ -38,7 +35,7 @@ public class AppointmentDTO { this.appointmentDate = appointmentDate; this.appointmentTime = appointmentTime; this.appointmentStatus = appointmentStatus; - this.petIds = petIds; + this.petId = petId; } public Long getAppointmentId() { @@ -89,12 +86,12 @@ public class AppointmentDTO { return appointmentStatus; } - public List getPetNames() { - return petNames; + public String getPetName() { + return petName; } - public List getPetIds() { - return petIds; + public Long getPetId() { + return petId; } public String getCreatedAt() { @@ -105,16 +102,8 @@ public class AppointmentDTO { return updatedAt; } - public String getPetName() { - return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : ""; - } - public Long getPetID() { - return (petIds != null && !petIds.isEmpty()) ? petIds.get(0) : null; - } - - public Long getPetId() { - return getPetID(); + return petId; } public String getServiceType() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index d497f529..cf9bb837 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -48,7 +48,6 @@ public class AppointmentDetailFragment extends Fragment { private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; - private final String[] STATUSES = {"Booked","Completed","Cancelled"}; private AppointmentViewModel appointmentViewModel; private PetViewModel petViewModel; @@ -95,7 +94,8 @@ public class AppointmentDetailFragment extends Fragment { * Configures the adapters for spinners. */ private void setupSpinners() { - SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, STATUSES); + SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerAppointmentStatus, + new String[]{"Booked", "Completed", "Cancelled", "Missed"}); String[] hours = new String[HOURS.length]; for (int i = 0; i < HOURS.length; i++) @@ -243,7 +243,7 @@ public class AppointmentDetailFragment extends Fragment { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { AppointmentDTO a = resource.data; - preselectedPetId = (a.getPetID() != null) ? a.getPetID() : -1; + preselectedPetId = (a.getPetId() != null) ? a.getPetId() : -1; preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; @@ -265,7 +265,12 @@ public class AppointmentDetailFragment extends Fragment { } catch (NumberFormatException ignored) {} } - SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, a.getAppointmentStatus()); + // Match Title labels with backend values + String status = a.getAppointmentStatus(); + if (status != null && !status.isEmpty()) { + String formattedStatus = status.substring(0, 1).toUpperCase() + status.substring(1).toLowerCase(); + SpinnerUtils.setSelectionByValue(binding.spinnerAppointmentStatus, formattedStatus); + } refreshPetSpinner(); refreshServiceSpinner(); @@ -306,11 +311,13 @@ public class AppointmentDetailFragment extends Fragment { String time = String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], MINUTES[binding.spinnerMinute.getSelectedItemPosition()]); - String status = STATUSES[binding.spinnerAppointmentStatus.getSelectedItemPosition()]; + + // Get status and convert to uppercase for backend + String status = binding.spinnerAppointmentStatus.getSelectedItem().toString().toUpperCase(); - // Validate future date+time if status is Booked - if ("Booked".equalsIgnoreCase(status)) { + // Validate future date+time if status is BOOKED + if ("BOOKED".equalsIgnoreCase(status)) { try { String[] dateParts = date.split("-"); String[] timeParts = time.split(":"); @@ -342,7 +349,7 @@ public class AppointmentDetailFragment extends Fragment { date, time, status, - Collections.singletonList(pet.getPetId()) + pet.getPetId() ); androidx.lifecycle.Observer> observer = resource -> { From 094c2d4a48145d31edbcfcd513f89bff53511e92 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:34:28 -0600 Subject: [PATCH 112/137] added filter options to appointments in the backend and andriod --- .../petstoremobile/api/AppointmentApi.java | 6 +- .../listfragments/AppointmentFragment.java | 151 +++++++++++++----- .../repositories/AppointmentRepository.java | 8 +- .../viewmodels/AppointmentViewModel.java | 8 +- .../main/res/layout/fragment_appointment.xml | 99 ++++++++++-- .../controller/AppointmentController.java | 15 +- .../repository/AppointmentRepository.java | 27 ++-- .../backend/service/AppointmentService.java | 43 +++-- 8 files changed, 261 insertions(+), 96 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java index 5d7044cf..483c66db 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java @@ -17,7 +17,11 @@ public interface AppointmentApi { @GET("api/v1/appointments") Call> getAllAppointments( @Query("page") int page, - @Query("size") int size); + @Query("size") int size, + @Query("q") String query, + @Query("status") String status, + @Query("storeId") Long storeId, + @Query("date") String date); @GET("api/v1/appointments/{id}") Call getAppointmentById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index c93b9e89..fab6a0b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -16,15 +16,21 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.Toast; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.AppointmentAdapter; +import com.example.petstoremobile.adapters.WhiteTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentAppointmentBinding; import com.example.petstoremobile.dtos.AppointmentDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.utils.EventDecorator; +import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -44,10 +50,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private FragmentAppointmentBinding binding; private List appointmentList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List storeList = new ArrayList<>(); private AppointmentAdapter adapter; private AppointmentViewModel appointmentViewModel; + private StoreViewModel storeViewModel; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; @@ -60,6 +67,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** @@ -72,9 +80,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupRecyclerView(); setupSearch(); + setupStatusFilter(); + setupStoreFilter(); setupSwipeRefresh(); setupCalendar(); - loadAppointmentData(); + setupFilterToggle(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -99,6 +109,13 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding = null; } + @Override + public void onResume() { + super.onResume(); + loadAppointmentData(); + loadStoreData(); + } + /** * Toggles the calendar between week and month display modes. */ @@ -109,6 +126,28 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } + /** + * Sets up the filter toggle button to show/hide the filter layout. + */ + private void setupFilterToggle() { + binding.btnToggleFilter.setOnClickListener(v -> { + if (binding.layoutFilter.getVisibility() == View.GONE) { + binding.layoutFilter.setVisibility(View.VISIBLE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_close_clear_cancel); + } else { + binding.layoutFilter.setVisibility(View.GONE); + binding.btnToggleFilter.setImageResource(android.R.drawable.ic_menu_search); + + // Reset filters when closing + binding.etSearchAppointment.setText(""); + binding.spinnerStatus.setSelection(0); + binding.spinnerStore.setSelection(0); + selectedCalendarDay = null; + binding.calendarView.clearSelection(); + } + }); + } + /** * Sets up the date selection listener for the calendar. */ @@ -124,7 +163,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } else { selectedCalendarDay = null; } - filterAppointments(binding.etSearchAppointment.getText().toString()); + loadAppointmentData(); }); } @@ -157,48 +196,56 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. */ private void setupSearch() { binding.etSearchAppointment.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterAppointments(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + loadAppointmentData(); } + @Override public void afterTextChanged(Editable s) {} }); } /** - * Filters the appointment list based on the search query and selected calendar date. + * Configures the status filter spinner. */ - private void filterAppointments(String query) { - filteredList.clear(); - String lowerQuery = query.toLowerCase(); + private void setupStatusFilter() { + String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"}; + WhiteTextArrayAdapter adapter = new WhiteTextArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + binding.spinnerStatus.setAdapter(adapter); - String selectedDateString = null; - if (selectedCalendarDay != null) { - selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", - selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); - } - - for (AppointmentDTO a : appointmentList) { - boolean matchesSearch = query.isEmpty() || - (a.getCustomerName() != null && a.getCustomerName().toLowerCase().contains(lowerQuery)) || - (a.getServiceType() != null && a.getServiceType().toLowerCase().contains(lowerQuery)) || - (a.getPetName() != null && a.getPetName().toLowerCase().contains(lowerQuery)); - - boolean matchesDate = (selectedDateString == null) || - (a.getAppointmentDate() != null && a.getAppointmentDate().equals(selectedDateString)); - - if (matchesSearch && matchesDate) { - filteredList.add(a); + binding.spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadAppointmentData(); } - } - adapter.notifyDataSetChanged(); + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Configures the store filter spinner. + */ + private void setupStoreFilter() { + binding.spinnerStore.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + loadAppointmentData(); + } + @Override public void onNothingSelected(AdapterView parent) {} + }); + } + + /** + * Fetches store data to populate the store filter. + */ + private void loadStoreData() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, storeList, + StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId); + } + }); } /** @@ -214,7 +261,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private void openAppointmentDetails(int position) { Bundle args = new Bundle(); if (position != -1) { - AppointmentDTO a = filteredList.get(position); + AppointmentDTO a = appointmentList.get(position); args.putLong("appointmentId", a.getAppointmentId()); } NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args); @@ -229,11 +276,27 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. } /** - * Fetches all appointment data from the server. + * Fetches appointment data from the server with all active filters. */ private void loadAppointmentData() { - //Load all appointments from the backend using viewModel - appointmentViewModel.getAllAppointments(0, 500).observe(getViewLifecycleOwner(), resource -> { + String query = binding.etSearchAppointment.getText().toString().trim(); + String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses"; + + Long storeId = null; + if (binding.spinnerStore.getSelectedItemPosition() > 0 && !storeList.isEmpty()) { + storeId = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId(); + } + + String selectedDateString = null; + if (selectedCalendarDay != null) { + selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d", + selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); + } + + if (status.equals("All Statuses")) status = null; + else status = status.toUpperCase(); + + appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data @@ -249,7 +312,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. appointmentList.clear(); appointmentList.addAll(resource.data.getContent()); updateCalendarDecorators(); - filterAppointments(binding.etSearchAppointment != null ? binding.etSearchAppointment.getText().toString() : ""); + adapter.notifyDataSetChanged(); } break; case ERROR: @@ -266,8 +329,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. * Initializes the RecyclerView for displaying appointments. */ private void setupRecyclerView() { - adapter = new AppointmentAdapter(filteredList, this); + adapter = new AppointmentAdapter(appointmentList, this); binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext())); binding.recyclerViewAppointments.setAdapter(adapter); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 30e25d0e..21c4fdc9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -21,10 +21,10 @@ public class AppointmentRepository extends BaseRepository { } /** - * Retrieves a paginated list of all appointments from the API. + * Retrieves a paginated list of all appointments from the API with filtering. */ - public LiveData>> getAllAppointments(int page, int size) { - return executeCall(appointmentApi.getAllAppointments(page, size)); + public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) { + return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date)); } /** @@ -54,4 +54,4 @@ public class AppointmentRepository extends BaseRepository { public LiveData> deleteAppointment(Long id) { return executeCall(appointmentApi.deleteAppointment(id)); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java index 23db67d0..c97dc97c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -22,10 +22,10 @@ public class AppointmentViewModel extends ViewModel { } /** - * Fetches a paginated list of all appointments. + * Fetches a paginated list of all appointments with optional filters. */ - public LiveData>> getAllAppointments(int page, int size) { - return repository.getAllAppointments(page, size); + public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) { + return repository.getAllAppointments(page, size, query, status, storeId, date); } /** @@ -55,4 +55,4 @@ public class AppointmentViewModel extends ViewModel { public LiveData> deleteAppointment(Long id) { return repository.deleteAppointment(id); } -} +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_appointment.xml b/android/app/src/main/res/layout/fragment_appointment.xml index 10e17d48..f8a98ec2 100644 --- a/android/app/src/main/res/layout/fragment_appointment.xml +++ b/android/app/src/main/res/layout/fragment_appointment.xml @@ -35,7 +35,8 @@ android:text="Appointments" android:textColor="@color/white" android:textSize="20sp" - android:textStyle="bold"/> + android:textStyle="bold" + android:layout_marginStart="8dp"/> + + + + + + + + + + + + + + + + + + + + + + + + - - > getAllAppointments( @RequestParam(required = false) String q, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String status, + @RequestParam(required = false) String date, + @RequestParam(required = false) Long customerId, + @RequestParam(required = false) Long employeeId, Pageable pageable) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String role = authentication.getAuthorities().stream() .findFirst() .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = null; + Long effectiveCustomerId = customerId; if (role != null && role.equals("CUSTOMER")) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); + effectiveCustomerId = user.getId(); } - return ResponseEntity.ok(appointmentService.getAllAppointments(q, pageable, customerId)); + LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; + + return ResponseEntity.ok(appointmentService.getAllAppointments( + q, effectiveCustomerId, employeeId, storeId, status, appointmentDate, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java index 00edebf9..dc7d40ef 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AppointmentRepository.java @@ -22,20 +22,25 @@ public interface AppointmentRepository extends JpaRepository List findByStoreAndDate(@Param("storeId") Long storeId, @Param("date") LocalDate date); @Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE " + + "(:q IS NULL OR (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchAppointments(@Param("q") String query, Pageable pageable); - - Page findByCustomerId(Long customerId, Pageable pageable); - - @Query("SELECT a FROM Appointment a LEFT JOIN a.pet p WHERE a.customer.id = :customerId AND (" + - "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.service.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')))") - Page searchAppointmentsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); + "LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%'))" + + ")) AND " + + "(:customerId IS NULL OR a.customer.id = :customerId) AND " + + "(:employeeId IS NULL OR a.employee.id = :employeeId) AND " + + "(:storeId IS NULL OR a.store.storeId = :storeId) AND " + + "(:status IS NULL OR LOWER(a.appointmentStatus) = LOWER(:status)) AND " + + "(:date IS NULL OR a.appointmentDate = :date)") + Page searchAppointments( + @Param("q") String query, + @Param("customerId") Long customerId, + @Param("employeeId") Long employeeId, + @Param("storeId") Long storeId, + @Param("status") String status, + @Param("date") LocalDate date, + Pageable pageable); @Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.employee.id = :employeeId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')") List findByEmployeeIdAndAppointmentDate(@Param("employeeId") Long employeeId, @Param("date") LocalDate date); diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index d7a65149..d037b057 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -45,22 +45,27 @@ public class AppointmentService { } @Transactional(readOnly = true) - public Page getAllAppointments(String query, Pageable pageable, Long customerId) { - Page appointments; + public Page getAllAppointments( + String query, + Long customerId, + Long employeeId, + Long storeId, + String status, + LocalDate date, + Pageable pageable) { - if (customerId != null) { - if (query != null && !query.trim().isEmpty()) { - appointments = appointmentRepository.searchAppointmentsByCustomer(customerId, query, pageable); - } else { - appointments = appointmentRepository.findByCustomerId(customerId, pageable); - } - } else { - if (query != null && !query.trim().isEmpty()) { - appointments = appointmentRepository.searchAppointments(query, pageable); - } else { - appointments = appointmentRepository.findAll(pageable); - } - } + String normalizedQuery = normalizeFilter(query); + String normalizedStatus = normalizeFilter(status); + + Page appointments = appointmentRepository.searchAppointments( + normalizedQuery, + customerId, + employeeId, + storeId, + normalizedStatus, + date, + pageable + ); return appointments.map(this::mapToResponse); } @@ -204,6 +209,14 @@ public class AppointmentService { return availableSlots; } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private void validateAppointmentRequest(AppointmentRequest request) { if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); From 0a55014f21b686d4d3df608565c810c292b757ae Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:48:36 -0600 Subject: [PATCH 113/137] added my appointments button for logged in user on andriod --- .../petstoremobile/api/AppointmentApi.java | 3 +- .../listfragments/AppointmentFragment.java | 35 ++++++++++++++++++- .../repositories/AppointmentRepository.java | 4 +-- .../viewmodels/AppointmentViewModel.java | 4 +-- .../main/res/layout/fragment_appointment.xml | 12 +++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java index 483c66db..d811b2e0 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/AppointmentApi.java @@ -21,7 +21,8 @@ public interface AppointmentApi { @Query("q") String query, @Query("status") String status, @Query("storeId") Long storeId, - @Query("date") String date); + @Query("date") String date, + @Query("employeeId") Long employeeId); @GET("api/v1/appointments/{id}") Call getAppointmentById(@Path("id") Long id); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java index fab6a0b5..3724850b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -30,6 +30,7 @@ import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.AppointmentViewModel; import com.example.petstoremobile.utils.EventDecorator; +import com.example.petstoremobile.viewmodels.AuthViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; import com.prolificinteractive.materialcalendarview.CalendarDay; import com.prolificinteractive.materialcalendarview.CalendarMode; @@ -55,9 +56,11 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. private AppointmentAdapter adapter; private AppointmentViewModel appointmentViewModel; private StoreViewModel storeViewModel; + private AuthViewModel authViewModel; private CalendarDay selectedCalendarDay; private boolean isMonthMode = false; + private Long currentUserId = null; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); /** @@ -68,6 +71,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. super.onCreate(savedInstanceState); appointmentViewModel = new ViewModelProvider(this).get(AppointmentViewModel.class); storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + authViewModel = new ViewModelProvider(this).get(AuthViewModel.class); } /** @@ -85,6 +89,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. setupSwipeRefresh(); setupCalendar(); setupFilterToggle(); + setupMyAppointmentFilter(); binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); @@ -100,6 +105,8 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode()); + loadCurrentUserInfo(); + return binding.getRoot(); } @@ -126,6 +133,26 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. .commit(); } + /** + * Sets up the "My Appointments" filter button. + */ + private void setupMyAppointmentFilter() { + binding.btnMyAppointments.setOnClickListener(v -> { + loadAppointmentData(); + }); + } + + /** + * Fetches current user info to get the employeeId. + */ + private void loadCurrentUserInfo() { + authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + currentUserId = resource.data.getId(); + } + }); + } + /** * Sets up the filter toggle button to show/hide the filter layout. */ @@ -142,6 +169,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. binding.etSearchAppointment.setText(""); binding.spinnerStatus.setSelection(0); binding.spinnerStore.setSelection(0); + binding.btnMyAppointments.setChecked(false); selectedCalendarDay = null; binding.calendarView.clearSelection(); } @@ -293,10 +321,15 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter. selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay()); } + Long employeeId = null; + if (binding.btnMyAppointments.isChecked()) { + employeeId = currentUserId; + } + if (status.equals("All Statuses")) status = null; else status = status.toUpperCase(); - appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString).observe(getViewLifecycleOwner(), resource -> { + appointmentViewModel.getAllAppointments(0, 500, query, status, storeId, selectedDateString, employeeId).observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; // Check the status to see if the resource is loaded and display the data diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java index 21c4fdc9..1c85e91a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/AppointmentRepository.java @@ -23,8 +23,8 @@ public class AppointmentRepository extends BaseRepository { /** * Retrieves a paginated list of all appointments from the API with filtering. */ - public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) { - return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date)); + public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { + return executeCall(appointmentApi.getAllAppointments(page, size, query, status, storeId, date, employeeId)); } /** diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java index c97dc97c..913d7ab2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/AppointmentViewModel.java @@ -24,8 +24,8 @@ public class AppointmentViewModel extends ViewModel { /** * Fetches a paginated list of all appointments with optional filters. */ - public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date) { - return repository.getAllAppointments(page, size, query, status, storeId, date); + public LiveData>> getAllAppointments(int page, int size, String query, String status, Long storeId, String date, Long employeeId) { + return repository.getAllAppointments(page, size, query, status, storeId, date, employeeId); } /** diff --git a/android/app/src/main/res/layout/fragment_appointment.xml b/android/app/src/main/res/layout/fragment_appointment.xml index f8a98ec2..8afc956c 100644 --- a/android/app/src/main/res/layout/fragment_appointment.xml +++ b/android/app/src/main/res/layout/fragment_appointment.xml @@ -130,6 +130,18 @@ + + Date: Tue, 7 Apr 2026 07:27:37 -0600 Subject: [PATCH 114/137] fixed creating adoption for the backend and implemented adoption to andriod for changes --- .../adapters/AdoptionAdapter.java | 2 +- .../petstoremobile/dtos/AdoptionDTO.java | 21 +++++++++-- .../AdoptionDetailFragment.java | 36 ++++++++++++++++++- .../res/layout/fragment_adoption_detail.xml | 16 ++++++++- .../controller/AdoptionController.java | 8 +++-- .../repository/AdoptionRepository.java | 26 +++++++------- .../backend/service/AdoptionService.java | 32 +++++++++-------- 7 files changed, 105 insertions(+), 36 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index c6bd678c..6dd8eeb4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -57,7 +57,7 @@ public class AdoptionAdapter extends RecyclerView.Adapter petList = new ArrayList<>(); private List customerList = new ArrayList<>(); + private List storeList = new ArrayList<>(); - private final String[] STATUSES = {"Pending", "Approved", "Rejected"}; + private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; private AdoptionViewModel adoptionViewModel; private PetViewModel petViewModel; private CustomerViewModel customerViewModel; + private StoreViewModel storeViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -51,6 +55,7 @@ public class AdoptionDetailFragment extends Fragment { adoptionViewModel = new ViewModelProvider(this).get(AdoptionViewModel.class); petViewModel = new ViewModelProvider(this).get(PetViewModel.class); customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } @Override @@ -107,6 +112,7 @@ public class AdoptionDetailFragment extends Fragment { private void loadSpinnersData() { loadPets(); loadCustomers(); + loadStores(); } /** @@ -152,6 +158,27 @@ public class AdoptionDetailFragment extends Fragment { preselectedCustomerId, CustomerDTO::getCustomerId); } + /** + * Loads the list of stores from the API. + */ + private void loadStores() { + storeViewModel.getAllStores(0, 200).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + refreshStoreSpinner(); + } + }); + } + + /** + * Populates the store selection spinner with data. + */ + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -182,11 +209,13 @@ public class AdoptionDetailFragment extends Fragment { AdoptionDTO a = resource.data; preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; + preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; binding.etAdoptionDate.setText(a.getAdoptionDate()); SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); refreshPetSpinner(); refreshCustomerSpinner(); + refreshStoreSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -203,6 +232,9 @@ public class AdoptionDetailFragment extends Fragment { if (binding.spinnerAdoptionPet.getSelectedItemPosition() == 0) { Toast.makeText(getContext(), "Select a pet", Toast.LENGTH_SHORT).show(); return; } + if (binding.spinnerAdoptionStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); return; + } String date = binding.etAdoptionDate.getText().toString().trim(); if (date.isEmpty()) { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; @@ -210,11 +242,13 @@ public class AdoptionDetailFragment extends Fragment { CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); + StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( pet.getPetId(), customer.getCustomerId(), + store.getStoreId(), date, status ); diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 2f6b153f..0ad14fcf 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -95,6 +95,20 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> + + + + - \ No newline at end of file + diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index bcb61db4..0e833dd5 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -33,6 +33,8 @@ public class AdoptionController { @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllAdoptions( @RequestParam(required = false) String q, + @RequestParam(required = false) Long customerId, + @RequestParam(required = false) String status, Pageable pageable) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String role = authentication.getAuthorities().stream() @@ -40,13 +42,13 @@ public class AdoptionController { .map(authority -> authority.getAuthority().replace("ROLE_", "")) .orElse(null); - Long customerId = null; + Long effectiveCustomerId = customerId; if (role != null && role.equals("CUSTOMER")) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); + effectiveCustomerId = user.getId(); } - return ResponseEntity.ok(adoptionService.getAllAdoptions(q, pageable, customerId)); + return ResponseEntity.ok(adoptionService.getAllAdoptions(q, effectiveCustomerId, status, pageable)); } @GetMapping("/{id}") diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 7502ec33..e3c21776 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -14,22 +14,24 @@ import java.util.Optional; public interface AdoptionRepository extends JpaRepository { @Query("SELECT a FROM Adoption a WHERE " + + "(:q IS NULL OR (" + "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))") - Page searchAdoptions(@Param("q") String query, Pageable pageable); - - Page findByCustomerId(Long customerId, Pageable pageable); - - @Query("SELECT a FROM Adoption a WHERE a.customer.id = :customerId AND (" + - "LOWER(a.customer.firstName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " + - "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))") - Page searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable); + "LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%'))" + + ")) AND " + + "(:customerId IS NULL OR a.customer.id = :customerId) AND " + + "(:status IS NULL OR LOWER(a.adoptionStatus) = LOWER(:status))") + Page searchAdoptions( + @Param("q") String query, + @Param("customerId") Long customerId, + @Param("status") String status, + Pageable pageable); Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); - boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); + @Query("SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END FROM Adoption a WHERE a.pet.id = :petId AND LOWER(a.adoptionStatus) = LOWER(:adoptionStatus) AND a.adoptionId <> :adoptionId") + boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(@Param("petId") Long petId, @Param("adoptionStatus") String adoptionStatus, @Param("adoptionId") Long adoptionId); - boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); + @Query("SELECT CASE WHEN COUNT(a) > 0 THEN true ELSE false END FROM Adoption a WHERE a.pet.id = :petId AND LOWER(a.adoptionStatus) = LOWER(:adoptionStatus)") + boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(@Param("petId") Long petId, @Param("adoptionStatus") String adoptionStatus); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 10b3fd02..ec9ef393 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -38,22 +38,16 @@ public class AdoptionService { this.storeRepository = storeRepository; } - public Page getAllAdoptions(String query, Pageable pageable, Long customerId) { - Page adoptions; + public Page getAllAdoptions(String query, Long customerId, String status, Pageable pageable) { + String normalizedQuery = normalizeFilter(query); + String normalizedStatus = normalizeFilter(status); - if (customerId != null) { - if (query != null && !query.trim().isEmpty()) { - adoptions = adoptionRepository.searchAdoptionsByCustomer(customerId, query, pageable); - } else { - adoptions = adoptionRepository.findByCustomerId(customerId, pageable); - } - } else { - if (query != null && !query.trim().isEmpty()) { - adoptions = adoptionRepository.searchAdoptions(query, pageable); - } else { - adoptions = adoptionRepository.findAll(pageable); - } - } + Page adoptions = adoptionRepository.searchAdoptions( + normalizedQuery, + customerId, + normalizedStatus, + pageable + ); return adoptions.map(this::mapToResponse); } @@ -140,6 +134,14 @@ public class AdoptionService { adoptionRepository.deleteAllById(request.getIds()); } + private String normalizeFilter(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private AdoptionResponse mapToResponse(Adoption adoption) { StoreLocation sourceStore = adoption.getSourceStore(); return new AdoptionResponse( From 8261cdfc2d847c06d9f2fd281954234514923699 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 07:46:22 -0600 Subject: [PATCH 115/137] added an api connection to Users in Andriod NOTE Will have to change backend so staffs can access other staffs --- .../example/petstoremobile/api/UserApi.java | 13 +++++++ .../petstoremobile/di/NetworkModule.java | 8 ++++- .../AppointmentDetailFragment.java | 35 +++++++++++++++++++ .../repositories/UserRepository.java | 26 ++++++++++++++ .../viewmodels/UserViewModel.java | 27 ++++++++++++++ .../layout/fragment_appointment_detail.xml | 18 ++++++++-- 6 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/UserApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java diff --git a/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java new file mode 100644 index 00000000..53611e66 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/UserApi.java @@ -0,0 +1,13 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.UserDTO; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface UserApi { + @GET("api/v1/users") + Call> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java index 0311840d..b5f1ca64 100644 --- a/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java +++ b/android/app/src/main/java/com/example/petstoremobile/di/NetworkModule.java @@ -173,4 +173,10 @@ public class NetworkModule { public static CategoryApi provideCategoryApi(Retrofit retrofit) { return retrofit.create(CategoryApi.class); } -} \ No newline at end of file + + @Provides + @Singleton + public static UserApi provideUserApi(Retrofit retrofit) { + return retrofit.create(UserApi.class); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java index cf9bb837..7757f156 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -21,6 +21,7 @@ import com.example.petstoremobile.viewmodels.CustomerViewModel; import com.example.petstoremobile.viewmodels.PetViewModel; import com.example.petstoremobile.viewmodels.ServiceViewModel; import com.example.petstoremobile.viewmodels.StoreViewModel; +import com.example.petstoremobile.viewmodels.UserViewModel; import java.util.*; @@ -40,11 +41,13 @@ public class AppointmentDetailFragment extends Fragment { private long preselectedServiceId = -1; private long preselectedCustomerId = -1; private long preselectedStoreId = -1; + private long preselectedStaffId = -1; private List petList = new ArrayList<>(); private List serviceList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); + private List staffList = new ArrayList<>(); private final Integer[] HOURS = {9,10,11,12,13,14,15,16,17}; private final Integer[] MINUTES = {0,15,30,45}; @@ -54,6 +57,7 @@ public class AppointmentDetailFragment extends Fragment { private ServiceViewModel serviceViewModel; private StoreViewModel storeViewModel; private CustomerViewModel customerViewModel; + private UserViewModel userViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -63,6 +67,7 @@ public class AppointmentDetailFragment extends Fragment { serviceViewModel = new ViewModelProvider(this).get(ServiceViewModel.class); storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); + userViewModel = new ViewModelProvider(this).get(UserViewModel.class); } @Override @@ -128,6 +133,7 @@ public class AppointmentDetailFragment extends Fragment { loadServices(); loadCustomers(); loadStores(); + loadStaff(); } /** @@ -215,6 +221,27 @@ public class AppointmentDetailFragment extends Fragment { preselectedStoreId, StoreDTO::getStoreId); } + /** + * Loads the list of staff from the API. + */ + private void loadStaff() { + userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + staffList = resource.data.getContent(); + refreshStaffSpinner(); + } + }); + } + + /** + * Populates the staff selection spinner. + */ + private void refreshStaffSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerStaff, staffList, + UserDTO::getFullName, "-- Select Staff --", + preselectedStaffId, UserDTO::getId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -247,6 +274,7 @@ public class AppointmentDetailFragment extends Fragment { preselectedServiceId = (a.getServiceId() != null) ? a.getServiceId() : -1; preselectedCustomerId = (a.getCustomerId() != null) ? a.getCustomerId() : -1; preselectedStoreId = (a.getStoreId() != null) ? a.getStoreId() : -1; + preselectedStaffId = (a.getEmployeeId() != null) ? a.getEmployeeId() : -1; binding.etAppointmentDate.setText(a.getAppointmentDate()); @@ -276,6 +304,7 @@ public class AppointmentDetailFragment extends Fragment { refreshServiceSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); + refreshStaffSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load appointment: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -307,6 +336,11 @@ public class AppointmentDetailFragment extends Fragment { StoreDTO store = storeList.get(binding.spinnerStore.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerPet.getSelectedItemPosition() - 1); ServiceDTO service = serviceList.get(binding.spinnerService.getSelectedItemPosition() - 1); + + Long employeeId = null; + if (binding.spinnerStaff.getSelectedItemPosition() > 0) { + employeeId = staffList.get(binding.spinnerStaff.getSelectedItemPosition() - 1).getId(); + } String time = String.format("%02d:%02d", HOURS[binding.spinnerHour.getSelectedItemPosition()], @@ -346,6 +380,7 @@ public class AppointmentDetailFragment extends Fragment { customer.getCustomerId(), store.getStoreId(), service.getServiceId(), + employeeId, date, time, status, diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java new file mode 100644 index 00000000..0ed9ced9 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/UserRepository.java @@ -0,0 +1,26 @@ +package com.example.petstoremobile.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.petstoremobile.api.UserApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class UserRepository extends BaseRepository { + private final UserApi userApi; + + @Inject + public UserRepository(UserApi userApi) { + super("UserRepository"); + this.userApi = userApi; + } + + public LiveData>> getUsers(String role, int page, int size) { + return executeCall(userApi.getUsers(role, page, size)); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java new file mode 100644 index 00000000..d839f6c4 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/UserViewModel.java @@ -0,0 +1,27 @@ +package com.example.petstoremobile.viewmodels; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.ViewModel; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.repositories.UserRepository; +import com.example.petstoremobile.utils.Resource; + +import javax.inject.Inject; + +import dagger.hilt.android.lifecycle.HiltViewModel; + +@HiltViewModel +public class UserViewModel extends ViewModel { + private final UserRepository userRepository; + + @Inject + public UserViewModel(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public LiveData>> getUsers(String role, int page, int size) { + return userRepository.getUsers(role, page, size); + } +} diff --git a/android/app/src/main/res/layout/fragment_appointment_detail.xml b/android/app/src/main/res/layout/fragment_appointment_detail.xml index ef1d8b33..1f31bc54 100644 --- a/android/app/src/main/res/layout/fragment_appointment_detail.xml +++ b/android/app/src/main/res/layout/fragment_appointment_detail.xml @@ -124,15 +124,27 @@ android:textSize="12sp" android:layout_marginBottom="4dp"/> - - - + + + + + Date: Tue, 7 Apr 2026 07:19:00 -0600 Subject: [PATCH 116/137] update postman collection --- backend/petshop-api.postman_collection.json | 453 +++++++++++++++++- .../repository/AdoptionRepository.java | 4 +- .../backend/service/AdoptionService.java | 6 +- 3 files changed, 438 insertions(+), 25 deletions(-) diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 1068f4e0..3b36221b 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -2,7 +2,7 @@ "info": { "name": "PetShop Complete Collection", "_postman_id": "petshop-api-complete-v1", - "description": "Complete API collection with all 95+ verified endpoints", + "description": "Complete API collection for all backend endpoints", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ @@ -124,7 +124,11 @@ }, { "key": "adoptedPetId", - "value": "4" + "value": "15" + }, + { + "key": "employeeId", + "value": "" } ], "item": [ @@ -221,6 +225,7 @@ "});", "var jsonData = pm.response.json();", "if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);", + "if (jsonData.id !== undefined) pm.collectionVariables.set('customerId', jsonData.id);", "if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);" ] } @@ -659,6 +664,37 @@ } ] }, + { + "name": "Dropdown: Pet Species", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/pet-species", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Create Pet", "request": { @@ -723,7 +759,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": 1\n}", + "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": {{customerId}}\n}", "options": { "raw": { "language": "json" @@ -1475,13 +1511,13 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ], "body": { "mode": "raw", - "raw": "{\n \"storeId\": 1,\n \"paymentMethod\": \"Card\",\n \"customerId\": 1,\n \"items\": [\n {\n \"prodId\": 1,\n \"quantity\": 2\n },\n {\n \"prodId\": 2,\n \"quantity\": 1\n }\n ],\n \"isRefund\": false\n}" + "raw": "{\n \"storeId\": {{storeId}},\n \"paymentMethod\": \"Card\",\n \"customerId\": {{customerId}},\n \"items\": [\n {\n \"prodId\": 7,\n \"quantity\": 2\n },\n {\n \"prodId\": 8,\n \"quantity\": 1\n }\n ],\n \"isRefund\": false\n}" } }, "event": [ @@ -1889,6 +1925,37 @@ } ] }, + { + "name": "Dropdown: Product Categories", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/dropdowns/product-categories", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, { "name": "Create Category", "request": { @@ -2119,7 +2186,7 @@ "name": "Check Appointment Availability", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments/availability?storeId=1&serviceId=1&date=2026-12-20", + "url": "{{baseUrl}}/api/v1/appointments/availability?storeId={{storeId}}&serviceId={{serviceId}}&date=2026-12-20", "header": [ { "key": "Content-Type", @@ -2150,7 +2217,55 @@ "name": "List Appointments", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/appointments", + "url": { + "raw": "{{baseUrl}}/api/v1/appointments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "appointments" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + }, + { + "key": "status", + "value": "Booked", + "disabled": true, + "description": "optional" + }, + { + "key": "date", + "value": "2026-12-20", + "disabled": true, + "description": "optional" + }, + { + "key": "customerId", + "value": "1", + "disabled": true, + "description": "optional" + }, + { + "key": "employeeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -2226,7 +2341,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", + "raw": "{\n \"customerId\": {{customerId}},\n \"storeId\": {{storeId}},\n \"serviceId\": {{serviceId}},\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petId\": {{petId}}\n}", "options": { "raw": { "language": "json" @@ -2268,7 +2383,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}", + "raw": "{\n \"customerId\": {{customerId}},\n \"storeId\": {{storeId}},\n \"serviceId\": {{serviceId}},\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petId\": {{petId}}\n}", "options": { "raw": { "language": "json" @@ -2472,7 +2587,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}", + "raw": "{\n \"petId\": {{adoptedPetId}},\n \"customerId\": {{customerId}},\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"sourceStoreId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -2514,7 +2629,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}", + "raw": "{\n \"petId\": {{adoptedPetId}},\n \"customerId\": {{customerId}},\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"sourceStoreId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -2625,7 +2740,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"saleId\": 1,\n \"reason\": \"Defective product\"\n}", + "raw": "{\n \"saleId\": {{saleId}},\n \"reason\": \"Defective product\",\n \"items\": [\n {\n \"prodId\": 7,\n \"quantity\": 1\n }\n ]\n}", "options": { "raw": { "language": "json" @@ -3104,6 +3219,46 @@ } } ] + }, + { + "name": "Update Conversation", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/chat/conversations/{{conversationId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{staffToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"CLOSED\"\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] } ] }, @@ -3762,6 +3917,186 @@ } ] }, + { + "name": "Employees", + "item": [ + { + "name": "List Employees", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Get Employee by ID", + "request": { + "method": "GET", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Create Employee", + "request": { + "method": "POST", + "url": "{{baseUrl}}/api/v1/employees", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"employee{{$timestamp}}\",\n \"password\": \"employee123\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"email\": \"employee{{$timestamp}}@petshop.com\",\n \"phone\": \"403-555-0200\",\n \"role\": \"STAFF\",\n \"staffRole\": \"GROOMER\",\n \"primaryStoreId\": 1,\n \"active\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});", + "var jsonData = pm.response.json();", + "if (jsonData.id) pm.collectionVariables.set('employeeId', jsonData.id);" + ] + } + } + ] + }, + { + "name": "Update Employee", + "request": { + "method": "PUT", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"firstName\": \"Jane\",\n \"lastName\": \"Smith\",\n \"email\": \"employee.updated@petshop.com\",\n \"phone\": \"403-555-0201\",\n \"role\": \"STAFF\",\n \"staffRole\": \"VET\",\n \"primaryStoreId\": 1,\n \"active\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + } + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});" + ] + } + } + ] + }, + { + "name": "Delete Employee", + "request": { + "method": "DELETE", + "url": "{{baseUrl}}/api/v1/employees/{{employeeId}}", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{adminToken}}", + "type": "text" + } + ] + }, + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "pm.test('Status code is 204', function () {", + " pm.response.to.have.status(204);", + "});" + ] + } + } + ] + } + ] + }, { "name": "Stores", "item": [ @@ -4119,7 +4454,31 @@ "name": "List Inventory", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/inventory", + "url": { + "raw": "{{baseUrl}}/api/v1/inventory", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "inventory" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -4195,7 +4554,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4237,7 +4596,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 12\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 12,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4308,7 +4667,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10\n}", + "raw": "{\n \"prodId\": {{productId}},\n \"quantity\": 10,\n \"storeId\": {{storeId}}\n}", "options": { "raw": { "language": "json" @@ -4528,7 +4887,7 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ] @@ -4559,7 +4918,7 @@ }, { "key": "Authorization", - "value": "Bearer {{staffToken}}", + "value": "Bearer {{adminToken}}", "type": "text" } ], @@ -4622,7 +4981,31 @@ "name": "List Purchase Orders", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/purchase-orders", + "url": { + "raw": "{{baseUrl}}/api/v1/purchase-orders", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "purchase-orders" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "storeId", + "value": "1", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", @@ -4689,7 +5072,37 @@ "name": "List Product Suppliers", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/product-suppliers", + "url": { + "raw": "{{baseUrl}}/api/v1/product-suppliers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "product-suppliers" + ], + "query": [ + { + "key": "q", + "value": "", + "disabled": true, + "description": "optional" + }, + { + "key": "productId", + "value": "{{productId}}", + "disabled": true, + "description": "optional" + }, + { + "key": "supplierId", + "value": "{{supplierId}}", + "disabled": true, + "description": "optional" + } + ] + }, "header": [ { "key": "Content-Type", diff --git a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java index 7502ec33..23ccc543 100644 --- a/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/AdoptionRepository.java @@ -29,7 +29,7 @@ public interface AdoptionRepository extends JpaRepository { Optional findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus); - boolean existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); + boolean existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(Long petId, String adoptionStatus, Long adoptionId); - boolean existsByPetPetIdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); + boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 10b3fd02..c7ef4eb2 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -197,8 +197,8 @@ public class AdoptionService { private void validatePetAvailability(Pet pet, Long adoptionId) { boolean adoptedElsewhere = adoptionId == null - ? adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) - : adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + ? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) + : adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); if (adoptedElsewhere) { throw new IllegalArgumentException("Selected pet has already been adopted"); } @@ -210,7 +210,7 @@ public class AdoptionService { private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId) { boolean completedElsewhere = adoptionId != null - && adoptionRepository.existsByPetPetIdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); + && adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) || completedElsewhere) { pet.setPetStatus(PET_STATUS_ADOPTED); } else { From d3563e1f75ea05d29d518ced7b852d3a12c60360 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:05:08 -0600 Subject: [PATCH 117/137] fix desktop appointments --- .../dto/appointment/AppointmentRequest.java | 28 +++++--------- .../dto/appointment/AppointmentResponse.java | 38 +++++-------------- .../controllers/AppointmentController.java | 10 +---- .../AppointmentDialogController.java | 3 +- 4 files changed, 22 insertions(+), 57 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java index c87b2e87..e8166411 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java @@ -2,11 +2,8 @@ package org.example.petshopdesktop.api.dto.appointment; import java.time.LocalDate; import java.time.LocalTime; -import java.util.List; public class AppointmentRequest { - private List petIds; - private List customerPetIds; private Long customerId; private Long storeId; private Long serviceId; @@ -14,26 +11,11 @@ public class AppointmentRequest { private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; + private Long petId; public AppointmentRequest() { } - public List getPetIds() { - return petIds; - } - - public void setPetIds(List petIds) { - this.petIds = petIds; - } - - public List getCustomerPetIds() { - return customerPetIds; - } - - public void setCustomerPetIds(List customerPetIds) { - this.customerPetIds = customerPetIds; - } - public Long getCustomerId() { return customerId; } @@ -89,4 +71,12 @@ public class AppointmentRequest { public void setAppointmentStatus(String appointmentStatus) { this.appointmentStatus = appointmentStatus; } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java index d1768a94..dd74554b 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java @@ -10,16 +10,14 @@ public class AppointmentResponse { private Long storeId; private String storeName; private Long serviceId; - private java.util.List petNames; - private java.util.List petIds; - private java.util.List customerPetNames; - private java.util.List customerPetIds; private String serviceName; private Long employeeId; private String employeeName; private LocalDate appointmentDate; private LocalTime appointmentTime; private String appointmentStatus; + private String petName; + private Long petId; public AppointmentResponse() { } @@ -72,36 +70,20 @@ public class AppointmentResponse { this.serviceId = serviceId; } - public java.util.List getPetNames() { - return petNames; + public String getPetName() { + return petName; } - public void setPetNames(java.util.List petNames) { - this.petNames = petNames; + public void setPetName(String petName) { + this.petName = petName; } - public java.util.List getPetIds() { - return petIds; + public Long getPetId() { + return petId; } - public void setPetIds(java.util.List petIds) { - this.petIds = petIds; - } - - public java.util.List getCustomerPetNames() { - return customerPetNames; - } - - public void setCustomerPetNames(java.util.List customerPetNames) { - this.customerPetNames = customerPetNames; - } - - public java.util.List getCustomerPetIds() { - return customerPetIds; - } - - public void setCustomerPetIds(java.util.List customerPetIds) { - this.customerPetIds = customerPetIds; + public void setPetId(Long petId) { + this.petId = petId; } public String getServiceName() { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index d183918a..039e4b4f 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -231,18 +231,12 @@ public class AppointmentController { } private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) { - Long petId = response.getCustomerPetIds() != null && !response.getCustomerPetIds().isEmpty() - ? response.getCustomerPetIds().get(0) - : response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; - String petName = response.getCustomerPetNames() != null && !response.getCustomerPetNames().isEmpty() - ? String.join(", ", response.getCustomerPetNames()) - : String.join(", ", response.getPetNames()); return new AppointmentDTO( response.getAppointmentId().intValue(), response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, response.getCustomerName(), - petId != null ? petId.intValue() : 0, - petName, + response.getPetId() != null ? response.getPetId().intValue() : 0, + response.getPetName(), response.getServiceId() != null ? response.getServiceId().intValue() : 0, response.getServiceName(), response.getEmployeeId() != null ? response.getEmployeeId().intValue() : 0, diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java index d21ad756..139255c6 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -21,7 +21,6 @@ import org.example.petshopdesktop.util.ActivityLogger; import java.time.LocalTime; import java.time.LocalDate; import java.util.List; -import java.util.Collections; import java.util.Objects; public class AppointmentDialogController { @@ -215,7 +214,7 @@ public class AppointmentDialogController { } AppointmentRequest request = new AppointmentRequest(); - request.setCustomerPetIds(Collections.singletonList(cbPet.getValue().getId())); + request.setPetId(cbPet.getValue().getId()); request.setCustomerId(cbCustomer.getValue().getId()); request.setStoreId(storeId); request.setServiceId(cbService.getValue().getId()); From 4d244cc1c5c3196a302301c25e1cd05329fbf287 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:10:11 -0600 Subject: [PATCH 118/137] fix web registration --- .../backend/controller/AuthController.java | 76 +++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index c237f27e..ec4f822b 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -13,6 +13,7 @@ import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.JwtUtil; import com.petshop.backend.service.AvatarStorageService; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.PhoneUtils; import jakarta.validation.Valid; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -53,19 +54,23 @@ public class AuthController { @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - if (userRepository.findByUsername(request.getUsername()).isPresent()) { + String username = trimToNull(request.getUsername()); + String email = trimToNull(request.getEmail()); + NameParts nameParts = splitFullName(request.getFullName()); + String phone = normalizePhone(request.getPhone()); + + if (userRepository.findByUsername(username).isPresent()) { Map error = new HashMap<>(); error.put("message", "Username already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } - if (userRepository.findByEmail(request.getEmail()).isPresent()) { + if (userRepository.findByEmail(email).isPresent()) { Map error = new HashMap<>(); error.put("message", "Email already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } - String phone = trimToNull(request.getPhone()); if (phone != null && userRepository.findByPhone(phone).isPresent()) { Map error = new HashMap<>(); error.put("message", "Phone already exists"); @@ -73,10 +78,12 @@ public class AuthController { } User user = new User(); - user.setUsername(request.getUsername()); + user.setUsername(username); user.setPassword(passwordEncoder.encode(request.getPassword())); - user.setEmail(request.getEmail()); - user.setFullName(request.getFullName()); + user.setEmail(email); + user.setFirstName(nameParts.firstName()); + user.setLastName(nameParts.lastName()); + user.setFullName(nameParts.fullName()); user.setPhone(phone); user.setRole(User.Role.CUSTOMER); user.setActive(true); @@ -143,31 +150,36 @@ public class AuthController { User user = getAuthenticatedUser(); boolean invalidateToken = false; - if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) { - if (userRepository.findByUsername(request.getUsername()).isPresent()) { + String username = trimToNull(request.getUsername()); + if (username != null && !username.equals(user.getUsername())) { + if (userRepository.findByUsername(username).isPresent()) { Map error = new HashMap<>(); error.put("message", "Username already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } - user.setUsername(request.getUsername()); + user.setUsername(username); invalidateToken = true; } - if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) { - if (userRepository.findByEmail(request.getEmail()).isPresent()) { + String email = trimToNull(request.getEmail()); + if (email != null && !email.equals(user.getEmail())) { + if (userRepository.findByEmail(email).isPresent()) { Map error = new HashMap<>(); error.put("message", "Email already exists"); return ResponseEntity.status(HttpStatus.CONFLICT).body(error); } - user.setEmail(request.getEmail()); + user.setEmail(email); } if (request.getFullName() != null) { - user.setFullName(request.getFullName()); + NameParts nameParts = splitFullName(request.getFullName()); + user.setFirstName(nameParts.firstName()); + user.setLastName(nameParts.lastName()); + user.setFullName(nameParts.fullName()); } if (request.getPhone() != null) { - String phone = trimToNull(request.getPhone()); + String phone = normalizePhone(request.getPhone()); if (!java.util.Objects.equals(phone, user.getPhone())) { if (phone != null && userRepository.findByPhone(phone) .filter(existing -> !existing.getId().equals(user.getId())) @@ -196,11 +208,15 @@ public class AuthController { private UserInfoResponse toUserInfoResponse(User user) { StoreLocation primaryStore = user.getPrimaryStore(); Long customerId = user.getRole() == User.Role.CUSTOMER ? user.getId() : null; + String fullName = user.getFullName(); + if (fullName == null || fullName.isBlank()) { + fullName = joinFullName(user.getFirstName(), user.getLastName()); + } return new UserInfoResponse( user.getId(), user.getUsername(), user.getEmail(), - user.getFullName(), + fullName, user.getPhone(), avatarStorageService.toOwnerAvatarUrl(user), user.getRole().name(), @@ -218,6 +234,36 @@ public class AuthController { return trimmed.isEmpty() ? null : trimmed; } + private String normalizePhone(String value) { + return trimToNull(PhoneUtils.normalize(trimToNull(value))); + } + + private NameParts splitFullName(String value) { + String normalized = trimToNull(value); + if (normalized == null) { + throw new IllegalArgumentException("Full name is required"); + } + String[] parts = normalized.split("\\s+", 2); + String firstName = parts[0]; + String lastName = parts.length > 1 ? parts[1] : ""; + return new NameParts(firstName, lastName, joinFullName(firstName, lastName)); + } + + private String joinFullName(String firstName, String lastName) { + String first = trimToNull(firstName); + String last = trimToNull(lastName); + if (first == null) { + return last == null ? null : last; + } + if (last == null) { + return first; + } + return first + " " + last; + } + + private record NameParts(String firstName, String lastName, String fullName) { + } + @PostMapping("/me/avatar") public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { User user = getAuthenticatedUser(); From 0cb2ecff02104550d91b84386167bde9013e0c2e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:15:01 -0600 Subject: [PATCH 119/137] add my pets api --- .../backend/controller/MyPetController.java | 76 ++++++++++++++++++ .../petshop/backend/dto/pet/MyPetRequest.java | 42 ++++++++++ .../backend/dto/pet/MyPetResponse.java | 61 +++++++++++++++ .../backend/repository/PetRepository.java | 3 + .../petshop/backend/service/PetService.java | 78 +++++++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/controller/MyPetController.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java diff --git a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java new file mode 100644 index 00000000..e43dbc42 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java @@ -0,0 +1,76 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.pet.MyPetRequest; +import com.petshop.backend.dto.pet.MyPetResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.PetService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/my-pets") +@PreAuthorize("isAuthenticated()") +public class MyPetController { + + private final PetService petService; + private final UserRepository userRepository; + + public MyPetController(PetService petService, UserRepository userRepository) { + this.petService = petService; + this.userRepository = userRepository; + } + + @GetMapping + public ResponseEntity> getMyPets() { + return ResponseEntity.ok(petService.getMyPets(currentUserId())); + } + + @PostMapping + public ResponseEntity createMyPet(@Valid @RequestBody MyPetRequest request) { + return ResponseEntity.ok(petService.createMyPet(currentUserId(), request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateMyPet(@PathVariable Long id, @Valid @RequestBody MyPetRequest request) { + return ResponseEntity.ok(petService.updateMyPet(currentUserId(), id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteMyPet(@PathVariable Long id) { + petService.deleteMyPet(currentUserId(), id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{id}/image") + public ResponseEntity uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { + try { + return ResponseEntity.ok(petService.uploadMyPetImage(currentUserId(), id, image)); + } catch (IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage())); + } catch (IOException ex) { + return ResponseEntity.badRequest().body(Map.of("message", "Failed to upload pet image: " + ex.getMessage())); + } + } + + private Long currentUserId() { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + return user.getId(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java new file mode 100644 index 00000000..17d08e2f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java @@ -0,0 +1,42 @@ +package com.petshop.backend.dto.pet; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class MyPetRequest { + + @NotBlank(message = "Pet name is required") + @Size(max = 50, message = "Pet name must not exceed 50 characters") + private String petName; + + @NotBlank(message = "Species is required") + @Size(max = 50, message = "Species must not exceed 50 characters") + private String species; + + @Size(max = 50, message = "Breed must not exceed 50 characters") + private String breed; + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java new file mode 100644 index 00000000..7063b2f8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java @@ -0,0 +1,61 @@ +package com.petshop.backend.dto.pet; + +public class MyPetResponse { + + private Long customerPetId; + private String petName; + private String species; + private String breed; + private String imageUrl; + + public MyPetResponse() { + } + + public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) { + this.customerPetId = customerPetId; + this.petName = petName; + this.species = species; + this.breed = breed; + this.imageUrl = imageUrl; + } + + public Long getCustomerPetId() { + return customerPetId; + } + + public void setCustomerPetId(Long customerPetId) { + this.customerPetId = customerPetId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index fd55fabe..73f05460 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -9,11 +9,14 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface PetRepository extends JpaRepository { List findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus); + List findAllByOwner_IdOrderByPetNameAsc(Long ownerId); + Optional findByIdAndOwner_Id(Long id, Long ownerId); @Query("SELECT p FROM Pet p WHERE " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 09dd402b..bbd39085 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -1,6 +1,8 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.pet.MyPetRequest; +import com.petshop.backend.dto.pet.MyPetResponse; import com.petshop.backend.dto.pet.PetRequest; import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.entity.Adoption; @@ -25,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.List; import java.util.Locale; @Service @@ -82,6 +85,50 @@ public class PetService { return mapToResponse(pet); } + @Transactional(readOnly = true) + public List getMyPets(Long ownerUserId) { + return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream() + .map(this::mapToMyPetResponse) + .toList(); + } + + @Transactional + public MyPetResponse createMyPet(Long ownerUserId, MyPetRequest request) { + User owner = userRepository.findById(ownerUserId) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + ownerUserId)); + Pet pet = new Pet(); + pet.setOwner(owner); + pet.setStore(null); + pet.setPetStatus("Owned"); + applyMyPetRequest(pet, request); + return mapToMyPetResponse(petRepository.save(pet)); + } + + @Transactional + public MyPetResponse updateMyPet(Long ownerUserId, Long petId, MyPetRequest request) { + Pet pet = findOwnedPet(ownerUserId, petId); + pet.setPetStatus("Owned"); + pet.setStore(null); + applyMyPetRequest(pet, request); + return mapToMyPetResponse(petRepository.save(pet)); + } + + @Transactional + public void deleteMyPet(Long ownerUserId, Long petId) { + Pet pet = findOwnedPet(ownerUserId, petId); + deleteStoredImageIfPresent(pet.getImageUrl()); + petRepository.delete(pet); + } + + @Transactional + public MyPetResponse uploadMyPetImage(Long ownerUserId, Long petId, MultipartFile file) throws IOException { + validateImageFile(file); + Pet pet = findOwnedPet(ownerUserId, petId); + deleteStoredImageIfPresent(pet.getImageUrl()); + pet.setImageUrl(catalogImageStorageService.storePetImage(file)); + return mapToMyPetResponse(petRepository.save(pet)); + } + @Transactional public PetResponse createPet(PetRequest request) { Pet pet = new Pet(); @@ -225,6 +272,11 @@ public class PetService { } } + private Pet findOwnedPet(Long ownerUserId, Long petId) { + return petRepository.findByIdAndOwner_Id(petId, ownerUserId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); + } + private void deleteStoredImageIfPresent(String storedImagePath) { if (storedImagePath == null || storedImagePath.isBlank()) { return; @@ -276,6 +328,32 @@ public class PetService { ); } + private MyPetResponse mapToMyPetResponse(Pet pet) { + return new MyPetResponse( + pet.getPetId(), + pet.getPetName(), + pet.getPetSpecies(), + pet.getPetBreed(), + pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null + ); + } + + private void applyMyPetRequest(Pet pet, MyPetRequest request) { + pet.setPetName(request.getPetName().trim()); + pet.setPetSpecies(request.getSpecies().trim()); + pet.setPetBreed(normalizeOptional(request.getBreed())); + pet.setPetAge(null); + pet.setPetPrice(null); + } + + private String normalizeOptional(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private void applyOwnerAndStore(Pet pet, PetRequest request) { if ("owned".equalsIgnoreCase(request.getPetStatus())) { if (request.getCustomerId() != null) { From f0025886e9643df66a907336f177bf3a76d2779b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:27:34 -0600 Subject: [PATCH 120/137] fix desktop adoption save --- .../petshopdesktop/api/dto/adoption/AdoptionRequest.java | 9 +++++++++ .../dialogcontrollers/AdoptionDialogController.java | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java index 830488f1..af596ade 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java @@ -6,6 +6,7 @@ public class AdoptionRequest { private Long petId; private Long customerId; private Long employeeId; + private Long sourceStoreId; private LocalDate adoptionDate; private String adoptionStatus; @@ -36,6 +37,14 @@ public class AdoptionRequest { this.employeeId = employeeId; } + public Long getSourceStoreId() { + return sourceStoreId; + } + + public void setSourceStoreId(Long sourceStoreId) { + this.sourceStoreId = sourceStoreId; + } + public LocalDate getAdoptionDate() { return adoptionDate; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java index 67e1073f..0240c128 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -190,10 +190,16 @@ public class AdoptionDialogController { if (errorMsg.isEmpty()) { try { + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId == null || storeId <= 0) { + throw new IllegalStateException("Store is not set for this account"); + } + AdoptionRequest request = new AdoptionRequest(); request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId()); request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId()); request.setEmployeeId(cbEmployee.getSelectionModel().getSelectedItem().getId()); + request.setSourceStoreId(storeId); request.setAdoptionDate(dpAdoptionDate.getValue()); request.setAdoptionStatus(cbAdoptionStatus.getValue()); From 3d0e7011c151fd0a1f96136d8ef1428e085f3e74 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:31:26 -0600 Subject: [PATCH 121/137] fix desktop user inventory crud --- .../api/dto/employee/EmployeeRequest.java | 9 +++++++++ .../api/dto/employee/EmployeeResponse.java | 13 +++++++++++-- .../api/dto/inventory/InventoryRequest.java | 9 +++++++++ .../api/dto/inventory/InventoryResponse.java | 18 ++++++++++++++++++ .../controllers/InventoryController.java | 4 ++-- .../controllers/StaffAccountsController.java | 12 ++++++++---- .../InventoryDialogController.java | 8 +++++++- .../StaffEditDialogController.java | 5 +++++ .../StaffRegisterDialogController.java | 5 +++++ 9 files changed, 74 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java index f047f641..dd2a7271 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java @@ -5,9 +5,12 @@ public class EmployeeRequest { private String password; private String firstName; private String lastName; + private String fullName; private String email; private String phone; private String role; + private String staffRole; + private Long primaryStoreId; private Boolean active; public String getUsername() { return username; } @@ -18,12 +21,18 @@ public class EmployeeRequest { public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { return fullName; } + public void setFullName(String fullName) { this.fullName = fullName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } + public String getStaffRole() { return staffRole; } + public void setStaffRole(String staffRole) { this.staffRole = staffRole; } + public Long getPrimaryStoreId() { return primaryStoreId; } + public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; } public Boolean getActive() { return active; } public void setActive(Boolean active) { this.active = active; } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java index 030488c1..f9ce7f96 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java @@ -3,6 +3,7 @@ package org.example.petshopdesktop.api.dto.employee; import java.time.LocalDateTime; public class EmployeeResponse { + private Long id; private Long employeeId; private Long userId; private String username; @@ -12,13 +13,17 @@ public class EmployeeResponse { private String email; private String phone; private String role; + private String staffRole; + private Long primaryStoreId; private Boolean active; private LocalDateTime createdAt; private LocalDateTime updatedAt; - public Long getEmployeeId() { return employeeId; } + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public Long getEmployeeId() { return employeeId != null ? employeeId : id; } public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } - public Long getUserId() { return userId; } + public Long getUserId() { return userId != null ? userId : id; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @@ -34,6 +39,10 @@ public class EmployeeResponse { public void setPhone(String phone) { this.phone = phone; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } + public String getStaffRole() { return staffRole; } + public void setStaffRole(String staffRole) { this.staffRole = staffRole; } + public Long getPrimaryStoreId() { return primaryStoreId; } + public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; } public Boolean getActive() { return active; } public void setActive(Boolean active) { this.active = active; } public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java index 41196003..935a713e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java @@ -3,6 +3,7 @@ package org.example.petshopdesktop.api.dto.inventory; public class InventoryRequest { private Long prodId; private Integer quantity; + private Long storeId; public InventoryRequest() { } @@ -22,4 +23,12 @@ public class InventoryRequest { public void setQuantity(Integer quantity) { this.quantity = quantity; } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java index 1767f751..176228b9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java @@ -7,6 +7,8 @@ public class InventoryResponse { private Long prodId; private String productName; private String categoryName; + private Long storeId; + private String storeName; private Integer quantity; private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -46,6 +48,22 @@ public class InventoryResponse { this.categoryName = categoryName; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public Integer getQuantity() { return quantity; } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java index f5f83461..06b2afa9 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java @@ -235,8 +235,8 @@ public class InventoryController { response.getProdId() != null ? response.getProdId().intValue() : 0, response.getProductName(), response.getCategoryName() != null ? response.getCategoryName() : "", - 0, - "N/A", + response.getStoreId() != null ? response.getStoreId().intValue() : 0, + response.getStoreName() != null ? response.getStoreName() : "", response.getQuantity() != null ? response.getQuantity() : 0, 0 ); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java index 603b85fa..bec351a4 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -186,10 +186,14 @@ public class StaffAccountsController { long userId = employee.getUserId() != null ? employee.getUserId() : 0L; long employeeId = employee.getEmployeeId() != null ? employee.getEmployeeId() : 0L; String username = employee.getUsername(); - String fullName = employee.getFullName() != null ? employee.getFullName() : ""; - String[] names = splitFullName(fullName); - String firstName = names[0]; - String lastName = names[1]; + String firstName = employee.getFirstName() != null ? employee.getFirstName() : ""; + String lastName = employee.getLastName() != null ? employee.getLastName() : ""; + if (firstName.isBlank() && lastName.isBlank()) { + String fullName = employee.getFullName() != null ? employee.getFullName() : ""; + String[] names = splitFullName(fullName); + firstName = names[0]; + lastName = names[1]; + } String email = employee.getEmail() != null ? employee.getEmail() : ""; String phone = employee.getPhone() != null ? employee.getPhone() : ""; String role = employee.getRole() != null ? employee.getRole() : "STAFF"; diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java index 0b410186..499b5ae8 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java @@ -20,6 +20,7 @@ import org.example.petshopdesktop.api.dto.inventory.InventoryResponse; import org.example.petshopdesktop.api.dto.product.ProductResponse; import org.example.petshopdesktop.api.endpoints.InventoryApi; import org.example.petshopdesktop.api.endpoints.ProductApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.Product; import org.example.petshopdesktop.util.ActivityLogger; @@ -127,6 +128,10 @@ public class InventoryDialogController { try { InventoryRequest request = new InventoryRequest(); Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem(); + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId == null || storeId <= 0) { + throw new IllegalStateException("Store is not set for this account"); + } request.setProdId((long) selectedProduct.getProdId()); int quantity; try { @@ -135,6 +140,7 @@ public class InventoryDialogController { throw new IllegalArgumentException("Invalid quantity format"); } request.setQuantity(quantity); + request.setStoreId(storeId); if (mode.equals("Add")) { InventoryApi.getInstance().createInventory(request); @@ -206,4 +212,4 @@ public class InventoryDialogController { lblMode.setText(mode + " Inventory"); lblInventoryId.setVisible(mode.equals("Edit")); } -} \ No newline at end of file +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java index 5ed6316e..5b8b4d6a 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java @@ -11,6 +11,7 @@ import javafx.stage.Stage; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.models.StaffAccount; import org.example.petshopdesktop.util.ActivityLogger; @@ -104,14 +105,18 @@ public class StaffEditDialogController { new Thread(() -> { try { + Long storeId = UserSession.getInstance().getStoreId(); EmployeeRequest request = new EmployeeRequest(); request.setUsername(username); request.setPassword(password.isEmpty() ? null : password); request.setFirstName(firstName); request.setLastName(lastName); + request.setFullName(firstName + " " + lastName); request.setEmail(email); request.setPhone(phone); request.setRole(staffAccount.getRole()); + request.setStaffRole("Staff"); + request.setPrimaryStoreId(storeId); request.setActive(staffAccount.isActive()); EmployeeApi.getInstance().updateEmployee(staffAccount.getEmployeeId(), request); diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java index 8d121dde..5be1a27e 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java @@ -11,6 +11,7 @@ import javafx.scene.control.TextField; import javafx.stage.Stage; import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.auth.UserSession; import org.example.petshopdesktop.Validator; import org.example.petshopdesktop.util.ActivityLogger; @@ -89,14 +90,18 @@ public class StaffRegisterDialogController { new Thread(() -> { try { + Long storeId = UserSession.getInstance().getStoreId(); EmployeeRequest request = new EmployeeRequest(); request.setUsername(username); request.setPassword(password); request.setFirstName(firstName); request.setLastName(lastName); + request.setFullName(firstName + " " + lastName); request.setEmail(email); request.setPhone(phone); request.setRole("STAFF"); + request.setStaffRole("Staff"); + request.setPrimaryStoreId(storeId); request.setActive(true); EmployeeApi.getInstance().createEmployee(request); From 7980a7b9309a85657c5365c6316cd27f59f3cebf Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:34:31 -0600 Subject: [PATCH 122/137] fix desktop chat --- .../api/dto/chat/MessageRequest.java | 36 ++++++++++++ .../api/dto/chat/MessageResponse.java | 36 ++++++++++++ .../controllers/ChatController.java | 56 +++++++++++++++++-- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java index a5c17ca4..cd8efedf 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java @@ -2,6 +2,10 @@ package org.example.petshopdesktop.api.dto.chat; public class MessageRequest { private String content; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageRequest() { } @@ -17,4 +21,36 @@ public class MessageRequest { public void setContent(String content) { this.content = content; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java index f81db82d..096a6e58 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java @@ -9,6 +9,10 @@ public class MessageResponse { private String content; private LocalDateTime timestamp; private Boolean isRead; + private String attachmentUrl; + private String attachmentName; + private String attachmentMimeType; + private Long attachmentSizeBytes; public MessageResponse() { } @@ -60,4 +64,36 @@ public class MessageResponse { public void setIsRead(Boolean isRead) { this.isRead = isRead; } + + public String getAttachmentUrl() { + return attachmentUrl; + } + + public void setAttachmentUrl(String attachmentUrl) { + this.attachmentUrl = attachmentUrl; + } + + public String getAttachmentName() { + return attachmentName; + } + + public void setAttachmentName(String attachmentName) { + this.attachmentName = attachmentName; + } + + public String getAttachmentMimeType() { + return attachmentMimeType; + } + + public void setAttachmentMimeType(String attachmentMimeType) { + this.attachmentMimeType = attachmentMimeType; + } + + public Long getAttachmentSizeBytes() { + return attachmentSizeBytes; + } + + public void setAttachmentSizeBytes(Long attachmentSizeBytes) { + this.attachmentSizeBytes = attachmentSizeBytes; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index bc343639..99fed184 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -5,6 +5,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; @@ -284,15 +285,35 @@ public class ChatController { Label author = new Label(resolveAuthorLabel(message)); author.setStyle("-fx-font-weight: bold; -fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); - Label content = new Label(message.getContent()); - content.setWrapText(true); - content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); - String timestampText = message.getTimestamp() == null ? "" : TIME_FORMATTER.format(message.getTimestamp()); Label timestamp = new Label(timestampText); timestamp.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#94a3b8") + "; -fx-font-size: 11px;"); - VBox bubble = new VBox(4, author, content, timestamp); + VBox bubble = new VBox(4, author); + String contentText = message.getContent() == null ? "" : message.getContent(); + if (!contentText.isBlank()) { + Label content = new Label(contentText); + content.setWrapText(true); + content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); + bubble.getChildren().add(content); + } + + if (message.getAttachmentUrl() != null && !message.getAttachmentUrl().isBlank()) { + String attachmentLabel = message.getAttachmentName(); + if (attachmentLabel == null || attachmentLabel.isBlank()) { + attachmentLabel = "Attachment"; + } + if (message.getAttachmentSizeBytes() != null && message.getAttachmentSizeBytes() > 0) { + attachmentLabel = attachmentLabel + " (" + formatSize(message.getAttachmentSizeBytes()) + ")"; + } + Hyperlink attachment = new Hyperlink(attachmentLabel); + attachment.setWrapText(true); + attachment.setOnAction(event -> openAttachment(message.getAttachmentUrl())); + attachment.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#0f766e") + ";"); + bubble.getChildren().add(attachment); + } + + bubble.getChildren().add(timestamp); bubble.setMaxWidth(420); bubble.setStyle(mine ? "-fx-background-color: #0f766e; -fx-background-radius: 14; -fx-padding: 12;" @@ -347,4 +368,29 @@ public class ChatController { private void scrollMessagesToBottom() { Platform.runLater(() -> spMessages.setVvalue(1.0)); } + + private void openAttachment(String url) { + try { + java.awt.Desktop.getDesktop().browse(java.net.URI.create(url)); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ChatController.openAttachment", + e, + "Opening chat attachment"); + } + } + + private String formatSize(Long bytes) { + if (bytes == null || bytes <= 0) { + return ""; + } + double size = bytes; + String[] units = {"B", "KB", "MB", "GB"}; + int unitIndex = 0; + while (size >= 1024 && unitIndex < units.length - 1) { + size = size / 1024; + unitIndex++; + } + return unitIndex == 0 ? String.format("%.0f %s", size, units[unitIndex]) : String.format("%.1f %s", size, units[unitIndex]); + } } From 89c706b893feadfd48ce1a30a0c454498dcfe278 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:38:17 -0600 Subject: [PATCH 123/137] stabilize desktop chat --- .../controllers/ChatController.java | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java index 99fed184..f9dc4940 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -5,7 +5,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; -import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; @@ -124,20 +123,28 @@ public class ChatController { @FXML void btnSendClicked() { - if (selectedConversation == null) { - lblChatStatus.setText("Select a conversation"); - return; - } + try { + if (selectedConversation == null) { + lblChatStatus.setText("Select a conversation"); + return; + } - String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim(); - if (content.isEmpty()) { - return; - } + String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim(); + if (content.isEmpty()) { + return; + } - txtMessage.clear(); - boolean sent = realtimeClient.sendMessage(selectedConversation.getId(), content); - if (!sent) { - sendMessageFallback(selectedConversation.getId(), content); + txtMessage.clear(); + boolean sent = realtimeClient.sendMessage(selectedConversation.getId(), content); + if (!sent) { + sendMessageFallback(selectedConversation.getId(), content); + } + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ChatController.btnSendClicked", + e, + "Sending chat message"); + lblChatStatus.setText("Chat send failed"); } } @@ -224,16 +231,30 @@ public class ChatController { private void renderMessages(List messages) { vbMessages.getChildren().clear(); for (MessageResponse message : messages) { - vbMessages.getChildren().add(createMessageBubble(message)); + try { + vbMessages.getChildren().add(createMessageBubble(message)); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ChatController.renderMessages", + e, + "Rendering chat message"); + } } scrollMessagesToBottom(); } private void appendMessageIfSelected(MessageResponse message) { - upsertConversationForMessage(message); - if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) { - vbMessages.getChildren().add(createMessageBubble(message)); - scrollMessagesToBottom(); + try { + upsertConversationForMessage(message); + if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) { + vbMessages.getChildren().add(createMessageBubble(message)); + scrollMessagesToBottom(); + } + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ChatController.appendMessageIfSelected", + e, + "Appending chat message"); } } @@ -306,10 +327,9 @@ public class ChatController { if (message.getAttachmentSizeBytes() != null && message.getAttachmentSizeBytes() > 0) { attachmentLabel = attachmentLabel + " (" + formatSize(message.getAttachmentSizeBytes()) + ")"; } - Hyperlink attachment = new Hyperlink(attachmentLabel); + Label attachment = new Label(attachmentLabel); attachment.setWrapText(true); - attachment.setOnAction(event -> openAttachment(message.getAttachmentUrl())); - attachment.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#0f766e") + ";"); + attachment.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#0f766e") + "; -fx-underline: true;"); bubble.getChildren().add(attachment); } @@ -368,18 +388,6 @@ public class ChatController { private void scrollMessagesToBottom() { Platform.runLater(() -> spMessages.setVvalue(1.0)); } - - private void openAttachment(String url) { - try { - java.awt.Desktop.getDesktop().browse(java.net.URI.create(url)); - } catch (Exception e) { - ActivityLogger.getInstance().logException( - "ChatController.openAttachment", - e, - "Opening chat attachment"); - } - } - private String formatSize(Long bytes) { if (bytes == null || bytes <= 0) { return ""; From baa143ff00eac46911eaac3143864ab19ba707ee Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:17:24 -0600 Subject: [PATCH 124/137] edited adapters in andriod to use viewbinding --- .../adapters/AdoptionAdapter.java | 42 +++++++-------- .../adapters/AppointmentAdapter.java | 42 +++++++-------- .../petstoremobile/adapters/ChatAdapter.java | 21 ++++---- .../adapters/InventoryAdapter.java | 51 ++++++++----------- .../adapters/MessageAdapter.java | 40 +++++++-------- .../petstoremobile/adapters/PetAdapter.java | 43 +++++++--------- .../adapters/ProductAdapter.java | 35 ++++++------- .../adapters/ProductSupplierAdapter.java | 29 +++++------ .../adapters/PurchaseOrderAdapter.java | 40 +++++++-------- .../petstoremobile/adapters/SaleAdapter.java | 44 +++++++--------- .../adapters/ServiceAdapter.java | 28 +++++----- .../adapters/SupplierAdapter.java | 28 +++++----- .../src/main/res/layout/item_appointment.xml | 9 ++++ 13 files changed, 204 insertions(+), 248 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index 6dd8eeb4..d4eddff1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -1,11 +1,11 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemAdoptionBinding; import com.example.petstoremobile.dtos.AdoptionDTO; import java.util.List; @@ -24,50 +24,46 @@ public class AdoptionAdapter extends RecyclerView.Adapter listener.onChatClick(chat)); } @@ -48,12 +46,11 @@ public class ChatAdapter extends RecyclerView.Adapter { if (selectionMode) { - toggleSelection(inv.getInventoryId(), holder.checkBox); + toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); } else { clickListener.onInventoryClick(holder.getAdapterPosition()); } @@ -105,14 +96,14 @@ public class InventoryAdapter extends RecyclerView.Adapter { if (!selectionMode) { selectionMode = true; - toggleSelection(inv.getInventoryId(), holder.checkBox); + toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); notifyDataSetChanged(); } return true; }); } - private void toggleSelection(Long id, CheckBox checkBox) { + private void toggleSelection(Long id, android.widget.CheckBox checkBox) { if (id == null) return; if (selectedIds.contains(id)) { diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java index de6ccc04..ee58941a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -12,6 +12,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemMessageReceivedBinding; +import com.example.petstoremobile.databinding.ItemMessageSentBinding; import com.example.petstoremobile.models.Message; import java.util.List; @@ -51,11 +53,11 @@ public class MessageAdapter extends RecyclerView.Adapter { // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { - TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; - ImageView ivPetProfile; + private final ItemPetBinding binding; - public PetViewHolder(@NonNull View v) { - super(v); - tvPetName = v.findViewById(R.id.tvPetName); - tvPetSpeciesBreed = v.findViewById(R.id.tvPetSpeciesBreed); - tvPetAge = v.findViewById(R.id.tvPetAge); - tvPetPrice = v.findViewById(R.id.tvPetPrice); - tvPetStatus = v.findViewById(R.id.tvPetStatus); - ivPetProfile = v.findViewById(R.id.ivPetProfile); + public PetViewHolder(@NonNull ItemPetBinding binding) { + super(binding.getRoot()); + this.binding = binding; } } @@ -61,41 +53,42 @@ public class PetAdapter extends RecyclerView.Adapter { @NonNull @Override public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pet, parent, false); - return new PetViewHolder(v); + ItemPetBinding binding = ItemPetBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new PetViewHolder(binding); } //populate the row with pet data @Override public void onBindViewHolder(@NonNull PetViewHolder holder, int position) { PetDTO pet = petList.get(position); + ItemPetBinding binding = holder.binding; - holder.tvPetName.setText(pet.getPetName()); - holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); - holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); + binding.tvPetName.setText(pet.getPetName()); + binding.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); + binding.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); Double price = pet.getPetPrice(); if (price != null) { - holder.tvPetPrice.setText("$" + String.format("%.2f", price)); + binding.tvPetPrice.setText("$" + String.format("%.2f", price)); } else { - holder.tvPetPrice.setText("$0.00"); + binding.tvPetPrice.setText("$0.00"); } - holder.tvPetStatus.setText(pet.getPetStatus()); + binding.tvPetStatus.setText(pet.getPetStatus()); //Set the status color depending on availability. If available, green, otherwise red if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) { - holder.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); } else { - holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); + binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); } // Load pet image using Glide if (baseUrl != null) { String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId()); - GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), holder.ivPetProfile, imageUrl, token, R.drawable.placeholder); + GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivPetProfile, imageUrl, token, R.drawable.placeholder); } else { - holder.ivPetProfile.setImageResource(R.drawable.placeholder); + binding.ivPetProfile.setImageResource(R.drawable.placeholder); } //when a row is clicked, open the detail view diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index ad1cf678..f5f897cc 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -1,13 +1,12 @@ package com.example.petstoremobile.adapters; import android.view.*; -import android.widget.ImageView; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.databinding.ItemProductBinding; import com.example.petstoremobile.dtos.ProductDTO; import com.example.petstoremobile.utils.GlideUtils; import java.util.List; @@ -37,41 +36,37 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java index 4c6377e1..75519120 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductSupplierAdapter.java @@ -1,10 +1,10 @@ package com.example.petstoremobile.adapters; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemProductSupplierBinding; import com.example.petstoremobile.dtos.ProductSupplierDTO; import java.util.List; @@ -23,30 +23,29 @@ public class ProductSupplierAdapter extends RecyclerView.Adapter listener.onProductSupplierClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java index 2d66e672..57061ef1 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PurchaseOrderAdapter.java @@ -1,11 +1,11 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; -import android.view.*; -import android.widget.TextView; +import android.view.LayoutInflater; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemPurchaseOrderBinding; import com.example.petstoremobile.dtos.PurchaseOrderDTO; import java.util.List; @@ -24,47 +24,45 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter saleClickListener.onSaleClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java index e3cc6d1c..15bc005d 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java @@ -1,12 +1,10 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemServiceBinding; import com.example.petstoremobile.dtos.ServiceDTO; import java.util.List; @@ -28,14 +26,11 @@ public class ServiceAdapter extends RecyclerView.Adapter serviceClickListener.onServiceClick(position)); diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java index e134f5b2..ce41d60e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java @@ -1,12 +1,10 @@ package com.example.petstoremobile.adapters; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.example.petstoremobile.R; +import com.example.petstoremobile.databinding.ItemSupplierBinding; import com.example.petstoremobile.dtos.SupplierDTO; import java.util.List; @@ -28,14 +26,11 @@ public class SupplierAdapter extends RecyclerView.Adapter supplierClickListener.onSupplierClick(position)); diff --git a/android/app/src/main/res/layout/item_appointment.xml b/android/app/src/main/res/layout/item_appointment.xml index 82e6ab2e..2844cb86 100644 --- a/android/app/src/main/res/layout/item_appointment.xml +++ b/android/app/src/main/res/layout/item_appointment.xml @@ -55,6 +55,15 @@ android:textColor="#666666" android:textSize="14sp" /> + + Date: Tue, 7 Apr 2026 14:35:57 -0600 Subject: [PATCH 125/137] updard Adoptions in andriod for new backend --- .../adapters/AdoptionAdapter.java | 1 + .../petstoremobile/dtos/AdoptionDTO.java | 67 ++++++++++++++++--- .../AdoptionDetailFragment.java | 53 ++++++++++++++- .../res/layout/fragment_adoption_detail.xml | 32 +++++++++ .../app/src/main/res/layout/item_adoption.xml | 12 ++++ 5 files changed, 154 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java index d4eddff1..259bde61 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -46,6 +46,7 @@ public class AdoptionAdapter extends RecyclerView.Adapter petList = new ArrayList<>(); private List customerList = new ArrayList<>(); private List storeList = new ArrayList<>(); + private List employeeList = new ArrayList<>(); private final String[] STATUSES = {"Pending", "Completed", "Cancelled"}; @@ -48,6 +52,7 @@ public class AdoptionDetailFragment extends Fragment { private PetViewModel petViewModel; private CustomerViewModel customerViewModel; private StoreViewModel storeViewModel; + private UserViewModel userViewModel; @Override public void onCreate(Bundle savedInstanceState) { @@ -56,6 +61,7 @@ public class AdoptionDetailFragment extends Fragment { petViewModel = new ViewModelProvider(this).get(PetViewModel.class); customerViewModel = new ViewModelProvider(this).get(CustomerViewModel.class); storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); + userViewModel = new ViewModelProvider(this).get(UserViewModel.class); } @Override @@ -113,6 +119,7 @@ public class AdoptionDetailFragment extends Fragment { loadPets(); loadCustomers(); loadStores(); + loadEmployees(); } /** @@ -179,6 +186,27 @@ public class AdoptionDetailFragment extends Fragment { preselectedStoreId, StoreDTO::getStoreId); } + /** + * Loads the list of employees from the API. + */ + private void loadEmployees() { + userViewModel.getUsers("STAFF", 0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + employeeList = resource.data.getContent(); + refreshEmployeeSpinner(); + } + }); + } + + /** + * Populates the employee selection spinner with data. + */ + private void refreshEmployeeSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerAdoptionEmployee, employeeList, + UserDTO::getFullName, "-- Select Staff --", + preselectedEmployeeId, UserDTO::getId); + } + /** * Handles arguments to determine if the fragment is in edit or add mode. */ @@ -210,12 +238,16 @@ public class AdoptionDetailFragment extends Fragment { preselectedPetId = a.getPetId() != null ? a.getPetId() : -1; preselectedCustomerId = a.getCustomerId() != null ? a.getCustomerId() : -1; preselectedStoreId = a.getSourceStoreId() != null ? a.getSourceStoreId() : -1; + preselectedEmployeeId = a.getEmployeeId() != null ? a.getEmployeeId() : -1; + binding.etAdoptionDate.setText(a.getAdoptionDate()); + binding.etAdoptionFee.setText(a.getAdoptionFee() != null ? a.getAdoptionFee().toString() : ""); SpinnerUtils.setSelectionByValue(binding.spinnerAdoptionStatus, a.getAdoptionStatus()); refreshPetSpinner(); refreshCustomerSpinner(); refreshStoreSpinner(); + refreshEmployeeSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load adoption: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -240,17 +272,36 @@ public class AdoptionDetailFragment extends Fragment { Toast.makeText(getContext(), "Select a date", Toast.LENGTH_SHORT).show(); return; } + BigDecimal fee = BigDecimal.ZERO; + String feeStr = binding.etAdoptionFee.getText().toString().trim(); + if (!feeStr.isEmpty()) { + try { + fee = new BigDecimal(feeStr); + } catch (NumberFormatException e) { + Toast.makeText(getContext(), "Invalid fee format", Toast.LENGTH_SHORT).show(); + return; + } + } + CustomerDTO customer = customerList.get(binding.spinnerAdoptionCustomer.getSelectedItemPosition() - 1); PetDTO pet = petList.get(binding.spinnerAdoptionPet.getSelectedItemPosition() - 1); StoreDTO store = storeList.get(binding.spinnerAdoptionStore.getSelectedItemPosition() - 1); + + Long employeeId = null; + if (binding.spinnerAdoptionEmployee.getSelectedItemPosition() > 0) { + employeeId = employeeList.get(binding.spinnerAdoptionEmployee.getSelectedItemPosition() - 1).getId(); + } + String status = STATUSES[binding.spinnerAdoptionStatus.getSelectedItemPosition()]; AdoptionDTO dto = new AdoptionDTO( pet.getPetId(), customer.getCustomerId(), + employeeId, store.getStoreId(), date, - status + status, + fee ); if (isEditing) { diff --git a/android/app/src/main/res/layout/fragment_adoption_detail.xml b/android/app/src/main/res/layout/fragment_adoption_detail.xml index 0ad14fcf..45ac673f 100644 --- a/android/app/src/main/res/layout/fragment_adoption_detail.xml +++ b/android/app/src/main/res/layout/fragment_adoption_detail.xml @@ -95,6 +95,21 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp"/> + + + + + + + + + + + + Date: Tue, 7 Apr 2026 14:36:20 -0600 Subject: [PATCH 126/137] update web packages --- web/package-lock.json | 114 +++++++++++++++++++++--------------------- web/package.json | 2 +- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2ccc06fb..d85b7abd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,7 +8,7 @@ "name": "threaded-pets", "version": "0.1.0", "dependencies": { - "next": "16.1.6", + "next": "^16.2.2", "react": "19.2.3", "react-dom": "19.2.3" }, @@ -1032,9 +1032,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", + "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1048,9 +1048,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", + "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", "cpu": [ "arm64" ], @@ -1064,9 +1064,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", + "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", "cpu": [ "x64" ], @@ -1080,9 +1080,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", + "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", "cpu": [ "arm64" ], @@ -1096,9 +1096,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", + "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", "cpu": [ "arm64" ], @@ -1112,9 +1112,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", + "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", "cpu": [ "x64" ], @@ -1128,9 +1128,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", + "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", "cpu": [ "x64" ], @@ -1144,9 +1144,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", + "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", "cpu": [ "arm64" ], @@ -1160,9 +1160,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", + "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", "cpu": [ "x64" ], @@ -1741,9 +1741,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2422,9 +2422,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3568,9 +3568,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4954,14 +4954,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", + "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.2", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -4973,15 +4973,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.2", + "@next/swc-darwin-x64": "16.2.2", + "@next/swc-linux-arm64-gnu": "16.2.2", + "@next/swc-linux-arm64-musl": "16.2.2", + "@next/swc-linux-x64-gnu": "16.2.2", + "@next/swc-linux-x64-musl": "16.2.2", + "@next/swc-win32-arm64-msvc": "16.2.2", + "@next/swc-win32-x64-msvc": "16.2.2", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5298,9 +5298,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6099,9 +6099,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/web/package.json b/web/package.json index 20ad5734..5cb43e9f 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "lint": "eslint" }, "dependencies": { - "next": "16.1.6", + "next": "^16.2.2", "react": "19.2.3", "react-dom": "19.2.3" }, From 0813bb4b44b91095b14e21ad71dced782b461636 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:55:43 -0600 Subject: [PATCH 127/137] Did the same to inventory --- .../adapters/InventoryAdapter.java | 13 +- .../petstoremobile/api/InventoryApi.java | 8 +- .../petstoremobile/dtos/InventoryDTO.java | 21 ++- .../petstoremobile/dtos/InventoryRequest.java | 31 ---- .../InventoryDetailFragment.java | 150 +++++++----------- .../repositories/InventoryRepository.java | 5 +- .../viewmodels/InventoryViewModel.java | 5 +- .../res/layout/fragment_inventory_detail.xml | 39 ++--- .../src/main/res/layout/item_inventory.xml | 29 +--- 9 files changed, 113 insertions(+), 188 deletions(-) delete mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 984ad065..729d1749 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -53,20 +53,15 @@ public class InventoryAdapter extends RecyclerView.Adapter createInventory(@Body InventoryRequest request); + Call createInventory(@Body InventoryDTO request); // PUT /api/v1/inventory/{id} @PUT("api/v1/inventory/{id}") - Call updateInventory(@Path("id") Long id, @Body InventoryRequest request); + Call updateInventory(@Path("id") Long id, @Body InventoryDTO request); // DELETE /api/v1/inventory/{id} @DELETE("api/v1/inventory/{id}") Call deleteInventory(@Path("id") Long id); // DELETE /api/v1/inventory (bulk delete) - @DELETE("api/v1/inventory") + @HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true) Call bulkDeleteInventory(@Body BulkDeleteRequest request); } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java index fe2ec542..ddafd045 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryDTO.java @@ -6,6 +6,8 @@ public class InventoryDTO { private Long prodId; private String productName; private String categoryName; + private Long storeId; + private String storeName; private Integer quantity; private String createdAt; private String updatedAt; @@ -14,8 +16,9 @@ public class InventoryDTO { } // Constructor for create/update requests (matches InventoryRequest) - public InventoryDTO(Long prodId, Integer quantity) { + public InventoryDTO(Long prodId, Long storeId, Integer quantity) { this.prodId = prodId; + this.storeId = storeId; this.quantity = quantity; } @@ -51,6 +54,22 @@ public class InventoryDTO { this.categoryName = categoryName; } + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + public Integer getQuantity() { return quantity; } diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java deleted file mode 100644 index f84dfb5f..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/InventoryRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.petstoremobile.dtos; - -public class InventoryRequest { - private Long prodId; - private Integer quantity; - - public InventoryRequest() { - } - - public InventoryRequest(Long prodId, Integer quantity) { - this.prodId = prodId; - this.quantity = quantity; - } - - public Long getProdId() { - return prodId; - } - - public void setProdId(Long prodId) { - this.prodId = prodId; - } - - public Integer getQuantity() { - return quantity; - } - - public void setQuantity(Integer quantity) { - this.quantity = quantity; - } -} - diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java index 1a32df6e..7a729b94 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -1,14 +1,9 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.Toast; import androidx.annotation.NonNull; @@ -18,15 +13,16 @@ import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.fragment.NavHostFragment; -import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.databinding.FragmentInventoryDetailBinding; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.ProductDTO; +import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; +import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.viewmodels.ProductViewModel; +import com.example.petstoremobile.viewmodels.StoreViewModel; import java.util.ArrayList; import java.util.List; @@ -43,20 +39,15 @@ public class InventoryDetailFragment extends Fragment { private InventoryViewModel inventoryViewModel; private ProductViewModel productViewModel; + private StoreViewModel storeViewModel; private boolean isEditing = false; private long inventoryId = -1; + private long preselectedStoreId = -1; + private long preselectedProductId = -1; - // The product selected from the dropdown - private ProductDTO selectedProduct = null; - - // For debouncing product search - private final Handler searchHandler = new Handler(Looper.getMainLooper()); - private Runnable searchRunnable; - - // Dropdown list - private final List productSuggestions = new ArrayList<>(); - private ArrayAdapter dropdownAdapter; + private List storeList = new ArrayList<>(); + private List productList = new ArrayList<>(); /** * Initializes the view models. @@ -66,6 +57,7 @@ public class InventoryDetailFragment extends Fragment { super.onCreate(savedInstanceState); inventoryViewModel = new ViewModelProvider(this).get(InventoryViewModel.class); productViewModel = new ViewModelProvider(this).get(ProductViewModel.class); + storeViewModel = new ViewModelProvider(this).get(StoreViewModel.class); } /** @@ -85,94 +77,64 @@ public class InventoryDetailFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - setupProductSearch(); + loadSpinnersData(); handleArguments(); binding.btnInventoryBack.setOnClickListener(v -> navigateBack()); binding.btnSaveInventory.setOnClickListener(v -> saveInventory()); binding.btnDeleteInventory.setOnClickListener(v -> confirmDelete()); - - // Setup dropdown adapter - dropdownAdapter = new BlackTextArrayAdapter<>(requireContext(), - android.R.layout.simple_dropdown_item_1line, new ArrayList<>()); - binding.etProductSearch.setAdapter(dropdownAdapter); - binding.etProductSearch.setThreshold(1); // start showing after 1 character } @Override public void onDestroyView() { super.onDestroyView(); - if (searchRunnable != null) { - searchHandler.removeCallbacks(searchRunnable); - } binding = null; } /** - * Sets up the product search dropdown. + * Fetches required data for spinners from the backend. */ - private void setupProductSearch() { - binding.etProductSearch.addTextChangedListener(new TextWatcher() { - @Override public void beforeTextChanged(CharSequence s, int i, int i1, int i2) { - } + private void loadSpinnersData() { + loadStores(); + loadProducts(); + } - @Override public void afterTextChanged(Editable s) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - // Clear selected product when user is typing again - selectedProduct = null; - binding.tvProductInfo.setVisibility(View.GONE); - - if (searchRunnable != null) - searchHandler.removeCallbacks(searchRunnable); - String query = s.toString().trim(); - if (query.isEmpty()) - return; - - searchRunnable = () -> searchProducts(query); - searchHandler.postDelayed(searchRunnable, 400); - } - }); - - // When user picks an item from the dropdown - binding.etProductSearch.setOnItemClickListener((parent, view, position, id) -> { - if (position < productSuggestions.size()) { - selectedProduct = productSuggestions.get(position); - // Show product details below the search box - binding.tvProductInfo.setText( - "ID: " + selectedProduct.getProdId() - + " • " + selectedProduct.getCategoryName()); - binding.tvProductInfo.setVisibility(View.VISIBLE); + /** + * Loads the list of stores for the spinner. + */ + private void loadStores() { + storeViewModel.getAllStores(0, 100).observe(getViewLifecycleOwner(), resource -> { + if (resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeList = resource.data.getContent(); + refreshStoreSpinner(); } }); } + private void refreshStoreSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryStore, storeList, + StoreDTO::getStoreName, "-- Select Store --", + preselectedStoreId, StoreDTO::getStoreId); + } + /** - * Searches for products matching the query from the backend. + * Loads the list of products for the spinner. */ - private void searchProducts(String query) { - if (getView() == null) return; - productViewModel.getAllProducts(query, null, 0, 20, "prodName").observe(getViewLifecycleOwner(), resource -> { + private void loadProducts() { + productViewModel.getAllProducts(null, null, 0, 500, "prodName").observe(getViewLifecycleOwner(), resource -> { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - productSuggestions.clear(); - productSuggestions.addAll(resource.data.getContent()); - - // Build display strings: "Product Name (ID: X)" - List names = new ArrayList<>(); - for (ProductDTO p : productSuggestions) { - names.add(p.getProdName() + " (ID: " + p.getProdId() + ")"); - } - - dropdownAdapter.clear(); - dropdownAdapter.addAll(names); - dropdownAdapter.notifyDataSetChanged(); - binding.etProductSearch.showDropDown(); + productList = resource.data.getContent(); + refreshProductSpinner(); } }); } + private void refreshProductSpinner() { + SpinnerUtils.populateSpinner(requireContext(), binding.spinnerInventoryProduct, productList, + ProductDTO::getProdName, "-- Select Product --", + preselectedProductId, ProductDTO::getProdId); + } + /** * Handles fragment arguments to determine if we are in edit or add mode. */ @@ -193,7 +155,6 @@ public class InventoryDetailFragment extends Fragment { isEditing = false; binding.tvInventoryMode.setText("Add Inventory"); binding.tvInventoryId.setVisibility(View.GONE); - binding.tvProductInfo.setVisibility(View.GONE); binding.btnDeleteInventory.setVisibility(View.GONE); binding.btnSaveInventory.setText("Add"); } @@ -207,20 +168,12 @@ public class InventoryDetailFragment extends Fragment { if (resource == null) return; if (resource.status == Resource.Status.SUCCESS && resource.data != null) { InventoryDTO inv = resource.data; - binding.etProductSearch.setText(inv.getProductName()); binding.etQuantity.setText(String.valueOf(inv.getQuantity())); - - if (inv.getProdId() != null) { - binding.tvProductInfo.setText( - "ID: " + inv.getProdId() - + " • " + inv.getCategoryName()); - binding.tvProductInfo.setVisibility(View.VISIBLE); - - selectedProduct = new ProductDTO(); - selectedProduct.setProdId(inv.getProdId()); - selectedProduct.setProdName(inv.getProductName()); - selectedProduct.setCategoryName(inv.getCategoryName()); - } + preselectedStoreId = inv.getStoreId() != null ? inv.getStoreId() : -1; + preselectedProductId = inv.getProdId() != null ? inv.getProdId() : -1; + + refreshStoreSpinner(); + refreshProductSpinner(); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Failed to load inventory: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -231,9 +184,12 @@ public class InventoryDetailFragment extends Fragment { * Validates input and saves the current inventory item details to the backend. */ private void saveInventory() { - if (selectedProduct == null) { - binding.etProductSearch.setError("Please select a product from the list"); - binding.etProductSearch.requestFocus(); + if (binding.spinnerInventoryStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Please select a store", Toast.LENGTH_SHORT).show(); + return; + } + if (binding.spinnerInventoryProduct.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Please select a product", Toast.LENGTH_SHORT).show(); return; } @@ -243,8 +199,10 @@ public class InventoryDetailFragment extends Fragment { } int quantity = Integer.parseInt(binding.etQuantity.getText().toString().trim()); + StoreDTO store = storeList.get(binding.spinnerInventoryStore.getSelectedItemPosition() - 1); + ProductDTO product = productList.get(binding.spinnerInventoryProduct.getSelectedItemPosition() - 1); - InventoryRequest request = new InventoryRequest(selectedProduct.getProdId(), quantity); + InventoryDTO request = new InventoryDTO(product.getProdId(), store.getStoreId(), quantity); setButtonsEnabled(false); if (isEditing) { diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java index 05719c38..159e4cea 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/InventoryRepository.java @@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.InventoryApi; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.utils.Resource; @@ -39,11 +38,11 @@ public class InventoryRepository extends BaseRepository { /** * Sends a request to the API to create a new inventory record. */ - public LiveData> createInventory(InventoryRequest request) { + public LiveData> createInventory(InventoryDTO request) { return executeCall(inventoryApi.createInventory(request)); } - public LiveData> updateInventory(Long id, InventoryRequest request) { + public LiveData> updateInventory(Long id, InventoryDTO request) { return executeCall(inventoryApi.updateInventory(id, request)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java index 02a5f1bb..3b5a8507 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/InventoryViewModel.java @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.CategoryDTO; import com.example.petstoremobile.dtos.InventoryDTO; -import com.example.petstoremobile.dtos.InventoryRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.repositories.CategoryRepository; @@ -50,14 +49,14 @@ public class InventoryViewModel extends ViewModel { /** * Creates a new inventory record. */ - public LiveData> createInventory(InventoryRequest request) { + public LiveData> createInventory(InventoryDTO request) { return inventoryRepository.createInventory(request); } /** * Updates an existing inventory record by ID. */ - public LiveData> updateInventory(Long id, InventoryRequest request) { + public LiveData> updateInventory(Long id, InventoryDTO request) { return inventoryRepository.updateInventory(id, request); } diff --git a/android/app/src/main/res/layout/fragment_inventory_detail.xml b/android/app/src/main/res/layout/fragment_inventory_detail.xml index 09047dc3..77994cac 100644 --- a/android/app/src/main/res/layout/fragment_inventory_detail.xml +++ b/android/app/src/main/res/layout/fragment_inventory_detail.xml @@ -67,7 +67,23 @@ android:layout_marginBottom="12dp" android:visibility="gone"/> - + + + + + + + - - + - - - + android:layout_marginBottom="16dp"/> @@ -55,31 +55,14 @@ - - - - - - + android:textSize="14sp" + android:textStyle="italic" /> Date: Tue, 7 Apr 2026 15:13:15 -0600 Subject: [PATCH 128/137] added helper class for bulk delete and mad pets have bulk delete --- .../adapters/InventoryAdapter.java | 78 +++----- .../petstoremobile/adapters/PetAdapter.java | 54 ++++- .../example/petstoremobile/api/PetApi.java | 6 + .../listfragments/InventoryFragment.java | 73 ++----- .../fragments/listfragments/PetFragment.java | 26 +++ .../repositories/PetRepository.java | 8 + .../utils/BulkDeleteHandler.java | 108 ++++++++++ .../petstoremobile/utils/DialogUtils.java | 12 ++ .../petstoremobile/utils/SelectionHelper.java | 66 ++++++ .../viewmodels/PetViewModel.java | 10 + .../app/src/main/res/layout/fragment_pet.xml | 31 +++ android/app/src/main/res/layout/item_pet.xml | 189 ++++++++++-------- 12 files changed, 476 insertions(+), 185 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java index 729d1749..40b7fe41 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -10,16 +10,16 @@ import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.databinding.ItemInventoryBinding; import com.example.petstoremobile.dtos.InventoryDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; +import com.example.petstoremobile.utils.SelectionHelper; -import java.util.ArrayList; import java.util.List; -public class InventoryAdapter extends RecyclerView.Adapter { +public class InventoryAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private final List inventoryList; private final OnInventoryClickListener clickListener; - private final List selectedIds = new ArrayList<>(); - private boolean selectionMode = false; + private final SelectionHelper selectionHelper; public interface OnInventoryClickListener { void onInventoryClick(int position); @@ -30,6 +30,27 @@ public class InventoryAdapter extends RecyclerView.Adapter inventoryList, OnInventoryClickListener clickListener) { this.inventoryList = inventoryList; this.clickListener = clickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + clickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); + } + + @Override + public List getSelectedIds() { + return selectionHelper.getSelectedIds(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); } public static class InventoryViewHolder extends RecyclerView.ViewHolder { @@ -71,66 +92,33 @@ public class InventoryAdapter extends RecyclerView.Adapter { - if (selectionMode) { - toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(inv.getInventoryId()); + notifyItemChanged(position); } else { clickListener.onInventoryClick(holder.getAdapterPosition()); } }); holder.itemView.setOnLongClickListener(v -> { - if (!selectionMode) { - selectionMode = true; - toggleSelection(inv.getInventoryId(), binding.cbSelectInventory); - notifyDataSetChanged(); + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(inv.getInventoryId()); } return true; }); } - private void toggleSelection(Long id, android.widget.CheckBox checkBox) { - if (id == null) - return; - if (selectedIds.contains(id)) { - selectedIds.remove(id); - checkBox.setChecked(false); - } else { - selectedIds.add(id); - checkBox.setChecked(true); - } - clickListener.onSelectionChanged(selectedIds.size()); - if (selectedIds.isEmpty()) { - selectionMode = false; - notifyDataSetChanged(); - } - } - - public List getSelectedIds() { - return new ArrayList<>(selectedIds); - } - - public void clearSelection() { - selectedIds.clear(); - selectionMode = false; - notifyDataSetChanged(); - } - - public boolean isInSelectionMode() { - return selectionMode; - } - @Override public int getItemCount() { return inventoryList.size(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java index 11a90b16..87dd95b5 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -2,6 +2,7 @@ package com.example.petstoremobile.adapters; import android.graphics.Color; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; @@ -10,25 +11,41 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.databinding.ItemPetBinding; import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.SelectionHelper; + import java.util.List; -public class PetAdapter extends RecyclerView.Adapter { +public class PetAdapter extends RecyclerView.Adapter implements BulkDeleteHandler.SelectableAdapter { private List petList; private OnPetClickListener petClickListener; private String baseUrl; private String token; + private final SelectionHelper selectionHelper; // Interface for pet click on recycler view public interface OnPetClickListener { void onPetClick(int position); + void onSelectionChanged(int selectedCount); } //Constructor public PetAdapter(List petList, OnPetClickListener petClickListener) { this.petList = petList; this.petClickListener = petClickListener; + this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() { + @Override + public void onSelectionChanged(int count) { + petClickListener.onSelectionChanged(count); + } + + @Override + public void onSelectionModeToggle(boolean selectionMode) { + notifyDataSetChanged(); + } + }); } public void setBaseUrl(String baseUrl) { @@ -39,6 +56,16 @@ public class PetAdapter extends RecyclerView.Adapter { this.token = token; } + @Override + public List getSelectedIds() { + return selectionHelper.getSelectedIds(); + } + + @Override + public void clearSelection() { + selectionHelper.clearSelection(); + } + // Get the controls of each row in recycler view public static class PetViewHolder extends RecyclerView.ViewHolder { private final ItemPetBinding binding; @@ -91,8 +118,31 @@ public class PetAdapter extends RecyclerView.Adapter { binding.ivPetProfile.setImageResource(R.drawable.placeholder); } + // Bulk delete selection mode + if (selectionHelper.isInSelectionMode()) { + binding.cbSelectPet.setVisibility(View.VISIBLE); + binding.cbSelectPet.setChecked(selectionHelper.isSelected(pet.getPetId())); + } else { + binding.cbSelectPet.setVisibility(View.GONE); + binding.cbSelectPet.setChecked(false); + } + //when a row is clicked, open the detail view - holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); + holder.itemView.setOnClickListener(v -> { + if (selectionHelper.isInSelectionMode()) { + selectionHelper.toggleSelection(pet.getPetId()); + notifyItemChanged(position); + } else { + petClickListener.onPetClick(position); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!selectionHelper.isInSelectionMode()) { + selectionHelper.startSelection(pet.getPetId()); + } + return true; + }); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index 7db2c1e3..acae1b51 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -1,5 +1,6 @@ package com.example.petstoremobile.api; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; @@ -8,6 +9,7 @@ import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.HTTP; import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.PUT; @@ -48,6 +50,10 @@ public interface PetApi { @DELETE("api/v1/pets/{id}") Call deletePet(@Path("id") Long id); + // Bulk delete pets + @HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true) + Call bulkDeletePets(@Body BulkDeleteRequest request); + // Upload pet image @Multipart @POST("api/v1/pets/{id}/image") diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java index 1fe9d188..0c0e07de 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -24,6 +24,7 @@ import com.example.petstoremobile.databinding.FragmentInventoryBinding; import com.example.petstoremobile.dtos.InventoryDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.viewmodels.InventoryViewModel; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; @@ -44,6 +45,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn private List storeList = new ArrayList<>(); private InventoryAdapter adapter; private InventoryViewModel viewModel; + private BulkDeleteHandler bulkDeleteHandler; // Pagination private int currentPage = 0; @@ -72,6 +74,7 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); loadInventory(true); loadStoreData(); @@ -87,11 +90,25 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn } }); - binding.btnBulkDelete.setOnClickListener(v -> confirmBulkDelete()); - return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + new BulkDeleteHandler.SelectableAdapter() { + @Override public List getSelectedIds() { return adapter.getSelectedIds(); } + @Override public void clearSelection() { adapter.clearSelection(); } + }, + "inventory item", + viewModel::bulkDeleteInventory, + () -> loadInventory(true) + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -243,50 +260,6 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn }); } - /** - * Displays a confirmation dialog before performing a bulk deletion of selected items. - */ - private void confirmBulkDelete() { - List ids = adapter.getSelectedIds(); - if (ids.isEmpty()) - return; - - new androidx.appcompat.app.AlertDialog.Builder(requireContext()) - .setTitle("Delete " + ids.size() + " item(s)?") - .setMessage("This cannot be undone.") - .setPositiveButton("Delete", (d, w) -> bulkDelete(ids)) - .setNegativeButton("Cancel", null) - .show(); - } - - /** - * Executes the bulk deletion of inventory items through the ViewModel. - */ - private void bulkDelete(List ids) { - viewModel.bulkDeleteInventory(ids).observe(getViewLifecycleOwner(), resource -> { - if (resource != null && resource.status != Resource.Status.LOADING) { - if (resource.status == Resource.Status.SUCCESS) { - adapter.clearSelection(); - hideBulkDeleteBar(); - loadInventory(true); - Toast.makeText(getContext(), ids.size() + " item(s) deleted", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); - } - } - }); - } - - /** - * Hides the bulk deletion UI bar. - */ - private void hideBulkDeleteBar() { - if (binding != null) { - binding.btnBulkDelete.setVisibility(View.GONE); - binding.tvSelectionCount.setVisibility(View.GONE); - } - } - /** * Navigates to the inventory detail screen for a specific item or to add a new one. */ @@ -322,12 +295,8 @@ public class InventoryFragment extends Fragment implements InventoryAdapter.OnIn */ @Override public void onSelectionChanged(int selectedCount) { - if (selectedCount > 0) { - binding.btnBulkDelete.setVisibility(View.VISIBLE); - binding.tvSelectionCount.setVisibility(View.VISIBLE); - binding.tvSelectionCount.setText(selectedCount + " selected"); - } else { - hideBulkDeleteBar(); + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(selectedCount); } } } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java index f6b46b0e..d0fdfa3e 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -25,6 +25,7 @@ import com.example.petstoremobile.databinding.FragmentPetBinding; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.dtos.StoreDTO; import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.utils.BulkDeleteHandler; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.viewmodels.PetViewModel; @@ -46,6 +47,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen private PetAdapter adapter; private PetViewModel viewModel; private StoreViewModel storeViewModel; + private BulkDeleteHandler bulkDeleteHandler; @Inject @Named("baseUrl") String baseUrl; @@ -74,6 +76,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen setupStoreFilter(); setupSwipeRefresh(); setupFilterToggle(); + setupBulkDelete(); binding.fabAddPet.setOnClickListener(v -> openPetDetails()); @@ -90,6 +93,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen return binding.getRoot(); } + private void setupBulkDelete() { + bulkDeleteHandler = new BulkDeleteHandler( + this, + binding.layoutBulkDelete, + binding.tvSelectionCount, + binding.btnBulkDelete, + new BulkDeleteHandler.SelectableAdapter() { + @Override public List getSelectedIds() { return adapter.getSelectedIds(); } + @Override public void clearSelection() { adapter.clearSelection(); } + }, + "pet", + viewModel::bulkDeletePets, + this::loadPetData + ); + } + @Override public void onDestroyView() { super.onDestroyView(); @@ -231,6 +250,13 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen openPetProfile(position); } + @Override + public void onSelectionChanged(int selectedCount) { + if (bulkDeleteHandler != null) { + bulkDeleteHandler.onSelectionChanged(selectedCount); + } + } + /** * Fetches pet data from the server with all active filters. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java index 88ac2295..019b5884 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/PetRepository.java @@ -3,6 +3,7 @@ package com.example.petstoremobile.repositories; import androidx.lifecycle.LiveData; import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.Resource; @@ -57,6 +58,13 @@ public class PetRepository extends BaseRepository { return executeCall(petApi.deletePet(id)); } + /** + * Sends a request to the API to delete multiple pet records. + */ + public LiveData> bulkDeletePets(BulkDeleteRequest request) { + return executeCall(petApi.bulkDeletePets(request)); + } + /** * Uploads an image file for a specific pet via the API. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java b/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java new file mode 100644 index 00000000..1e777f5d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/BulkDeleteHandler.java @@ -0,0 +1,108 @@ +package com.example.petstoremobile.utils; + +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LiveData; + +import java.util.List; + +/** + * A helper class to handle the UI and logic for bulk deletion across different fragments. + */ +public class BulkDeleteHandler { + + /** + * Interface that adapters must implement to support bulk selection. + */ + public interface SelectableAdapter { + List getSelectedIds(); + void clearSelection(); + } + + /** + * Functional interface for the API call execution. + */ + public interface BulkDeleteOperation { + LiveData> execute(List ids); + } + + private final Fragment fragment; + private final View layoutBar; + private final TextView tvCount; + private final SelectableAdapter adapter; + private final BulkDeleteOperation operation; + private final Runnable onSuccess; + private final String itemName; + + public BulkDeleteHandler(Fragment fragment, + View layoutBar, + TextView tvCount, + Button btnDelete, + SelectableAdapter adapter, + String itemName, + BulkDeleteOperation operation, + Runnable onSuccess) { + this.fragment = fragment; + this.layoutBar = layoutBar; + this.tvCount = tvCount; + this.adapter = adapter; + this.operation = operation; + this.onSuccess = onSuccess; + this.itemName = itemName; + + btnDelete.setOnClickListener(v -> confirmDelete()); + } + + /** + * Updates the UI when the selection count changes. + */ + public void onSelectionChanged(int selectedCount) { + if (selectedCount > 0) { + layoutBar.setVisibility(View.VISIBLE); + tvCount.setText(selectedCount + " selected"); + } else { + hideBar(); + } + } + + /** + * Hides the bulk delete bar and resets state. + */ + public void hideBar() { + if (layoutBar != null) { + layoutBar.setVisibility(View.GONE); + } + } + + /** + * Shows the confirmation dialog. + */ + private void confirmDelete() { + List ids = adapter.getSelectedIds(); + if (ids.isEmpty()) return; + + DialogUtils.showBulkDeleteConfirmDialog(fragment.requireContext(), ids.size(), () -> performDelete(ids)); + } + + /** + * Executes the deletion via the provided operation. + */ + private void performDelete(List ids) { + operation.execute(ids).observe(fragment.getViewLifecycleOwner(), resource -> { + if (resource != null && resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + adapter.clearSelection(); + hideBar(); + onSuccess.run(); + Toast.makeText(fragment.getContext(), ids.size() + " " + itemName + "(s) deleted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(fragment.getContext(), "Delete failed: " + resource.message, Toast.LENGTH_SHORT).show(); + } + } + }); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java index 55436846..bf304d1c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java +++ b/android/app/src/main/java/com/example/petstoremobile/utils/DialogUtils.java @@ -34,6 +34,18 @@ public class DialogUtils { showConfirmDialog(context, "Delete " + itemName + "?", "Are you sure you want to delete this " + itemName.toLowerCase() + "? This action cannot be undone.", callback); } + /** + * Shows a confirmation dialog with specific "Delete" and "Cancel" buttons. + */ + public static void showBulkDeleteConfirmDialog(Context context, int count, DialogCallback callback) { + new AlertDialog.Builder(context) + .setTitle("Delete " + count + " item(s)?") + .setMessage("This cannot be undone.") + .setPositiveButton("Delete", (dialog, which) -> callback.onConfirm()) + .setNegativeButton("Cancel", null) + .show(); + } + /** * Shows a simple information or error dialog with an "OK" button. */ diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java new file mode 100644 index 00000000..cbda2267 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/SelectionHelper.java @@ -0,0 +1,66 @@ +package com.example.petstoremobile.utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to manage selection state in Adapters for bulk operations. + */ +public class SelectionHelper { + + private final List selectedIds = new ArrayList<>(); + private boolean selectionMode = false; + private final SelectionListener listener; + + public interface SelectionListener { + void onSelectionChanged(int count); + void onSelectionModeToggle(boolean selectionMode); + } + + public SelectionHelper(SelectionListener listener) { + this.listener = listener; + } + + public void toggleSelection(Long id) { + if (id == null) return; + + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + + listener.onSelectionChanged(selectedIds.size()); + + if (selectedIds.isEmpty() && selectionMode) { + selectionMode = false; + listener.onSelectionModeToggle(false); + } + } + + public void startSelection(Long id) { + selectionMode = true; + selectedIds.add(id); + listener.onSelectionChanged(selectedIds.size()); + listener.onSelectionModeToggle(true); + } + + public boolean isSelected(Long id) { + return selectedIds.contains(id); + } + + public boolean isInSelectionMode() { + return selectionMode; + } + + public List getSelectedIds() { + return new ArrayList<>(selectedIds); + } + + public void clearSelection() { + selectedIds.clear(); + selectionMode = false; + listener.onSelectionChanged(0); + listener.onSelectionModeToggle(false); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java index b0af57c8..4866b79a 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetViewModel.java @@ -3,11 +3,14 @@ package com.example.petstoremobile.viewmodels; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; +import com.example.petstoremobile.dtos.BulkDeleteRequest; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.repositories.PetRepository; import com.example.petstoremobile.utils.Resource; +import java.util.List; + import javax.inject.Inject; import dagger.hilt.android.lifecycle.HiltViewModel; @@ -57,6 +60,13 @@ public class PetViewModel extends ViewModel { return repository.deletePet(id); } + /** + * Deletes multiple pet records. + */ + public LiveData> bulkDeletePets(List ids) { + return repository.bulkDeletePets(new BulkDeleteRequest(ids)); + } + /** * Uploads an image for a specific pet. */ diff --git a/android/app/src/main/res/layout/fragment_pet.xml b/android/app/src/main/res/layout/fragment_pet.xml index 9273c0f0..fcd9ccba 100644 --- a/android/app/src/main/res/layout/fragment_pet.xml +++ b/android/app/src/main/res/layout/fragment_pet.xml @@ -132,6 +132,37 @@ + + + + +