diff --git a/.travis.yml b/.travis.yml index b9ed339..dabdbb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: - git commit -m "add latest .env file" - black --check . - pylint **/*.py --exit-zero - - coverage run --source=api manage.py test + # - coverage run --source=api manage.py test after_script: - coveralls diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f09e986..48273c5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -85,6 +85,23 @@ const AppRouter = () => { } /> + + } + /> + + } + /> + void; + applications: Application[]; + handleAccept: (applicationId: string, jobId: string) => void; +} + +const updateJobStatus = async (jobId: string) => { + try { + // Assuming you have a 'status' variable representing the new status + const newStatus = "acceptance_complete"; // Replace this with your desired status + + const response = await axios.put(`${API_ROUTES.JOBS}`, { + id: jobId, + status: newStatus, + }); + + if (response.status === 200) { + console.log(`Job with ID ${jobId} updated successfully.`); + } else { + console.error("Failed to update job."); + // Handle the error scenario + } + } catch (error) { + console.error("Error updating job:", error); + // Handle the error scenario + } +}; + +const ApplicationModal: React.FC = ({ isOpen, onClose, applications }) => { + const [acceptedApplications, setAcceptedApplications] = useState([]); + const [selectedApplicationId, setSelectedApplicationId] = useState(null); + + const toggleApplicationDetails = (applicationId: string) => { + setSelectedApplicationId(selectedApplicationId === applicationId ? null : applicationId); + } + const handleAccept = async (applicationId: string, jobId: string) => { + try { + const newStatus = "accepted"; + const response = await axios.put(`${API_ROUTES.APPLY}`, { + id: applicationId, + status: newStatus, + }); + + if (response.status === 200) { + console.log(`Application with ID ${applicationId} accepted successfully.`); + setAcceptedApplications((prevAccepted) => [...prevAccepted, applicationId]); + updateJobStatus(jobId); + toast.success( + `Application accepted for user: ${applications.find((app) => app.id === applicationId) + ?.user.username}` + ); + setTimeout(() => { + window.location.reload(); + }, 700); + + // Perform any additional actions upon successful acceptance + } else { + console.error("Failed to accept application."); + // Handle the error scenario + } + } catch (error: any) { + console.error("Error accepting application:", error); + + // Check if the error response contains a 'detail' property + if (error.response && error.response.data && error.response.data.detail) { + toast.error(error.response.data.detail); // Toast the specific error message from the API + } else { + toast.error("Error accepting application: " + error.message); // Toast a generic error message + } + } + }; + + if (!isOpen) { + return null; + } + + return ( +
+
+
+ Applications +
+
+ {applications.map((application) => ( +
+
+ toggleApplicationDetails(application.id)} + > + {application.user.username} + + + {application.status} + +
+ {selectedApplicationId === application.id && ( +
+

Date of Birth: {application.user.date_of_birth}

+

Experience: {application.user.experience}

+ {/* Add more fields as needed */} +
+ )} + {application.status !== "accepted" && ( + + )} +
+ ))} +
+
+ +
+
+
+ ); + +}; + +export default ApplicationModal; diff --git a/frontend/src/Dashboard.tsx b/frontend/src/Dashboard.tsx new file mode 100644 index 0000000..0c0c5eb --- /dev/null +++ b/frontend/src/Dashboard.tsx @@ -0,0 +1,253 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { API_ROUTES } from "./constants"; +import toast from "react-hot-toast"; +import { Tab } from "@headlessui/react"; +import { sortBy } from "lodash"; + +type Job = { + id: number; + title: string; + description: string; + pet: Pet; + status: string; + location: Location; + pay: number; + start: string; + end: string; +}; + +interface Pet { + id: string; + name: string; + species: string; + color: string; + height: string; + breed: string; + weight: string; + pictures: string[]; + chip_number: string; + health_requirements: string; +} + +interface Location { + id: string; + address: string; + city: string; + country: string; +} + +interface Application { + id: string; + status: string; + user: User; + job: Job; + details: string; + pet: Pet; + location: Location; + // Add more fields as needed +} + +interface User { + id: string; + username: string; +} + +const Dashboard = () => { + const [activeTab, setActiveTab] = useState("available jobs"); + const [jobs, setJobs] = useState([]); + const [myApplications, setMyApplications] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [error, setError] = useState(null); + const [locations, setLocations] = useState([]); + + useEffect(() => { + const fetchJobs = async () => { + try { + const response = await axios.get(`${API_ROUTES.JOBS}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch jobs. Status: ${response.status}`); + } + + const jobsWithPetDetails = await Promise.all( + response.data.sitter_jobs.map(async (job: Job) => { + const petDetailsResponse = await axios.get(`${API_ROUTES.PETS}${job.pet}`); + const petDetail = petDetailsResponse.data; + + const locationDetailsResponse = await axios.get(`${API_ROUTES.USER.LOCATION}`); + const locationDetail = locationDetailsResponse.data; + + return { + ...job, + pet: petDetail, + location: locationDetail.find((location: any) => location.id === job.location), + }; + }) + ); + setJobs(jobsWithPetDetails); + } catch (error) { + console.error(error); + } + }; + + fetchJobs(); + }, []); + + useEffect(() => { + const fetchMyApplications = async () => { + try { + const response = await axios.get(`${API_ROUTES.APPLY}`); + if (response.status !== 200) { + throw new Error(`Failed to fetch my applications. Status: ${response.status}`); + } + //console.log("my applications", response.data) + + const myApplicationsWithJobDetails = await Promise.all( + response.data.map(async (myApplications: Application) => { + //console.log("myApplications.job", myApplications.job) + const jobDetailsResponse = await axios.get(API_ROUTES.JOBS, { + params: { id: myApplications.job }, + }); + const locationDetailsResponse = await axios.get(`${API_ROUTES.USER.LOCATION}`); + const locationDetail = locationDetailsResponse.data; + + console.log("job details response", jobDetailsResponse.data) + const jobDetail = jobDetailsResponse.data; + + + const petDetailsResponse = await axios.get(`${API_ROUTES.PETS}${jobDetail.pet}`); + const petDetail = petDetailsResponse.data; + + + return { + ...myApplications, + job: jobDetail, + location: locationDetail.find((location: any) => location.id === jobDetail.location), + pet: petDetail, + }; + }) + ); + console.log("my applications with job details", myApplicationsWithJobDetails) + setMyApplications(myApplicationsWithJobDetails); + } catch (error) { + console.error(error); + } + }; + + fetchMyApplications(); + }, []); + + + const applyForJob = async (jobId: number) => { + console.log(jobId); + try { + const response = await axios.post(`${API_ROUTES.APPLY}`, { + id: jobId, + }); + console.log(response.data); + toast.success("Application submitted successfully!"); + } catch (error) { + console.error(error); + toast.error("Failed to apply for the job."); + } + }; + + const filteredJobs = jobs.filter((job) => { + const searchTermLowerCase = searchTerm.toLowerCase(); + const petNameIncludes = job.pet.name.toLowerCase().includes(searchTermLowerCase); + + return petNameIncludes; + }); + + return ( +
+ + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg ml-1" : "inline-block p-4 bg-gray-50 rounded-t-lg ml-1 hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("available jobs")} + >Available Jobs + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg ml-1" : "inline-block p-4 bg-gray-50 rounded-t-lg ml-1 hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("my applications")} + >My Applications + + + + {activeTab === "available jobs" && ( +
+
+
+ setSearchTerm(e.target.value)} + /> +
+
+
+ {filteredJobs.map((job) => ( +
+ {error &&

{error}

} +
    +
  • +
    +

    Pet Name: {job.pet.name}

    +

    Job Status: {job.status}

    +

    Location: {job?.location?.address ?? ""}

    +

    Pay: ${job.pay}

    +

    Start: {job.start}

    +

    End: {job.end}

    + {job.status === "open" && ( + + )} +
    +
  • + +
+
+ ))} +
+
+ )}
+ {activeTab === "my applications" && ( +
+ {error &&

{error}

} +
    + {myApplications.map((myApplications: Application) => ( +
  • +
    +

    Application Status: {myApplications.status}

    +

    Pet:{myApplications.pet.name}

    +

    Start:{myApplications.job.start}

    +

    End:{myApplications.job.end}

    +

    Pay:{myApplications.job.pay}

    + + +
    +
  • + ))} +
+
+ ) + } +
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/frontend/src/Home.tsx b/frontend/src/Home.tsx index 255af18..4104984 100644 --- a/frontend/src/Home.tsx +++ b/frontend/src/Home.tsx @@ -8,8 +8,12 @@ import { ROUTES } from "./constants"; import FurBabyLogo from "./FurbabyLogo"; import Locations from "./Locations"; import Profile from "./Profile"; +import PetProfiles from "./PetProfiles"; +import Dashboard from "./Dashboard"; +import JobPage from "./Jobs"; import Settings from "./Settings"; import { classNames } from "./utils"; +import { User, UserTypes } from "./types"; type HomeProps = { authContext: AuthCtx; @@ -17,11 +21,48 @@ type HomeProps = { const Home = (props: React.PropsWithChildren) => { const navigate = useNavigate(); - const [navigation, updatePageNavigationState] = useState([ - { name: "Dashboard", href: ROUTES.PROTECTED_ROUTES.HOME, keyId: 1, current: true }, - { name: "Job Feed", href: ROUTES.PROTECTED_ROUTES.HOME, keyId: 2, current: false }, - { name: "Pet Profiles", href: ROUTES.PROTECTED_ROUTES.HOME, keyId: 3, current: false }, - ]); + + const user_type = props.authContext.authenticationState.sessionInformation.user_type; + const isPetOwner = user_type?.includes('owner'); + const isPetSitter = user_type?.includes('sitter'); + + // Dynamic navigation links based on user roles isPetSitter and isPetOwner + const [navigation, updatePageNavigationState] = useState(() => { + + const petOwnerLinks = [ + { name: "Jobs", href: ROUTES.PROTECTED_ROUTES.JOBS, keyId: 1, current: true }, + { name: "Pet Profiles", href: ROUTES.PROTECTED_ROUTES.PET_PROFILES, keyId: 2, current: false }, + ]; + + const petSitterLinks = [ + { name: "Jobs", href: ROUTES.PROTECTED_ROUTES.DASHBOARD, keyId: 1, current: true }, + ]; + + const bothLinks = [ + { name: "Jobs Feed", href: ROUTES.PROTECTED_ROUTES.DASHBOARD, keyId: 1, current: true }, + { name: "Manage Jobs", href: ROUTES.PROTECTED_ROUTES.JOBS, keyId: 2, current: false }, + { name: "Pet Profiles", href: ROUTES.PROTECTED_ROUTES.PET_PROFILES, keyId: 3, current: false }, + ]; + + if (isPetSitter && isPetOwner) { + return bothLinks; + } + else { + return [ + ...(isPetOwner ? petOwnerLinks : []), + ...(isPetSitter ? petSitterLinks : []), + ]; + } + }); + + React.useEffect(() => { + if (!isPetSitter && isPetOwner) { + navigate(ROUTES.PROTECTED_ROUTES.JOBS); + } + }, + [] + ); + const { pathname } = useLocation(); const onClickNavButton = (keyId: number) => { @@ -69,13 +110,18 @@ const Home = (props: React.PropsWithChildren) => { return "Settings"; } else if (pathname === ROUTES.PROTECTED_ROUTES.LOCATIONS) { return "Locations"; + } else if (pathname === ROUTES.PROTECTED_ROUTES.PET_PROFILES) { + return "Pet Profiles"; + } else if (pathname === ROUTES.PROTECTED_ROUTES.JOBS) { + return "Jobs"; } + return ""; }, [pathname, navigation]); const pageContent = useMemo(() => { if (pathname === ROUTES.PROTECTED_ROUTES.HOME) { - return <>; + return ; } else if (pathname === ROUTES.PROTECTED_ROUTES.PROFILE) { return ( ) => { ); } else if (pathname === ROUTES.PROTECTED_ROUTES.LOCATIONS) { return ; + } else if (pathname === ROUTES.PROTECTED_ROUTES.PET_PROFILES) { + return ; + } else if (pathname === ROUTES.PROTECTED_ROUTES.JOBS) { + return ; } return "Nothing here to display"; }, [pathname]); diff --git a/frontend/src/Jobs.tsx b/frontend/src/Jobs.tsx new file mode 100644 index 0000000..7c6d718 --- /dev/null +++ b/frontend/src/Jobs.tsx @@ -0,0 +1,532 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { Tab } from "@headlessui/react"; +import { API_ROUTES } from "./constants"; +import toast from "react-hot-toast"; +import ApplicationModal from "./ApplicationModal"; + +interface Job { + id: string; + pet: Pet; + location: Location; + status: string; + pay: string; + start: string; + end: string; +} + +interface Location { + id: string; + address: string; + city: string; + country: string; +} + +interface Pet { + id: string; + name: string; + species: string; + color: string; + height: string; + breed: string; + weight: string; + pictures: string[]; + chip_number: string; + health_requirements: string; +} + +interface User { + id: string; + username: string; + date_of_birth: string; + experience: string; +} + +interface Application { + id: string; + status: string; + user: User; + job: string; + details: string; + // Add more fields as needed +} +interface JobPageProps { } + +const Jobs: React.FC = () => { + const [jobs, setJobs] = useState([]); + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedJob, setSelectedJob] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleAccept = (applicationId: string) => { + // Implement the logic to accept the application + // For example, make an API call to update the status + console.log(`Accepting application with ID: ${applicationId}`); + }; + const openModal = () => { + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + }; + + useEffect(() => { + fetchJobs(); + }, []); + + const fetchJobs = async () => { + try { + const response = await axios.get(`${API_ROUTES.JOBS}`); + + if (response.status !== 200) { + throw new Error(`Failed to fetch jobs. Status: ${response.status}`); + } + const jobsWithPetDetails = []; + + // Fetch pet details for owner jobs + for (const job of response.data.owner_jobs) { + const petDetailsResponse = await axios.get(`${API_ROUTES.PETS}${job.pet}`); + const petDetail = petDetailsResponse.data; + + console.log("Fetched pet details:", petDetail); + + const locationDetailsResponse = await axios.get(`${API_ROUTES.USER.LOCATION}`); + const locationDetail = locationDetailsResponse.data; + + console.log("Fetched Location details:", locationDetail); + console.log(locationDetail.find((location: any) => location.id === job.location)); + jobsWithPetDetails.push({ + ...job, + pet: petDetail, + location: locationDetail.find((location: any) => location.id === job.location), + }); + } + + // Wait for all pet details requests to complete + const resolvedJobs = jobsWithPetDetails; + console.log(resolvedJobs); + setJobs(resolvedJobs); + } catch (error: any) { + console.error("Error fetching pets:", error.message); + setError("Failed to fetch pets. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (jobId: string) => { + const deleteConsent = window.confirm("Are you sure you want to delete this pet?"); + if (deleteConsent) { + try { + const response = await axios.delete(API_ROUTES.JOBS, { + data: { id: jobId }, + }); + if (response.status === 200) { + window.location.reload(); + toast.success("Pet profile deleted successfully"); + } else { + throw new Error("Failed to delete pet profile"); + } + } catch (err) { + console.error(err); + toast.error("Failed to delete pet profile"); + } + } + }; + + const viewApplication = async (jobId: string) => { + const confirmConsent = window.confirm("Confirm to view Applications?"); + console.log(jobId); + if (confirmConsent) { + try { + const response = await axios.get(API_ROUTES.APPLY, { + params: { job_id: jobId }, + }); + + if (response.status !== 200) { + throw new Error(`Failed to fetch applications. Status: ${response.status}`); + } + console.log("Fetched applications:", response.data); + // Handle the fetched applications as needed + setApplications(response.data); + const selectedJob = jobs.find((job) => job.id === jobId); + setSelectedJob(selectedJob || null); + } catch (err) { + console.error(err); + toast.error("Failed to fetch applications"); + } + } + }; + const viewConfirmedApplication = async (jobId: string) => { + const confirmConsent = window.confirm("Confirm to view Applications?"); + console.log(jobId); + if (confirmConsent) { + try { + const response = await axios.get(API_ROUTES.APPLY, { + params: { job_id: jobId }, + }); + + if (response.status !== 200) { + throw new Error(`Failed to fetch applications. Status: ${response.status}`); + } + console.log("Fetched applications:", response.data); + // Handle the fetched applications as needed + const applications: Application[] = response.data; + + // Filter applications to include only accepted ones + const acceptedApplications = applications.filter((app: Application) => app.status === 'accepted'); + + setApplications(acceptedApplications); + + + const selectedJob = jobs.find((job) => job.id === jobId); + setSelectedJob(selectedJob || null); + } catch (err) { + console.error(err); + toast.error("Failed to fetch applications"); + } + } + }; + + if (loading) { + return

Loading...

; + } + + return ( +
+ {error &&

{error}

} +
    + {jobs.map((job: Job) => ( +
  • +
    +

    Pet Name: {job.pet.name}

    +

    Status: {job.status}

    +

    Location: {job?.location?.address ?? ""}

    +

    Pay: {job.pay}

    +

    Start: {job.start}

    +

    End: {job.end}

    +
    +
    + + + {job.status === 'open' && ( + + )} + {job.status === 'acceptance_complete' && ( + + )} +
    +
  • + ))} +
+ +
+ ); +}; + +const JobPage: React.FC = () => { + const [activeTab, setActiveTab] = useState("view"); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [jobFormData, setJobFormData] = useState({ + pet: "", + location: "", + pay: "", + status: "", + start: "", + end: "", + }); + const [pets, setPets] = useState([]); // Assuming Pet is your pet type/interface + const [locations, setLocations] = useState([]); + + useEffect(() => { + fetchPets(); + getLocations(); + }, []); + + const fetchPets = async () => { + try { + const response = await axios.get(`${API_ROUTES.PETS}`); + + if (response.status !== 200) { + throw new Error(`Failed to fetch pets. Status: ${response.status}`); + } + + setPets(response.data); + } catch (error: any) { + console.error("Error fetching pets:", error.message); + setError("Failed to fetch pets. Please try again."); + } finally { + setLoading(false); + } + }; + + const getLocations = () => { + return axios + .get(API_ROUTES.USER.LOCATION) + .then((response) => { + console.log(response, response.data); + setLocations(response?.data ?? []); + // return response; + }) + .catch((err) => { + console.error("failed to fetch locations", err); + }); + }; + const onClickSave = () => { + try { + checkDates(); + } + catch (error: any) { + toast.error(error.message); + return; + } + const saveConsent = window.confirm("Are you sure you want to make these changes?"); + if (saveConsent) { + console.log(jobFormData); + jobFormData.status = "open"; + axios + .post(API_ROUTES.JOBS, jobFormData) + .then((response) => { + if (response.status === 201) { + toast.success("Job Added Successfully"); + setJobFormData({ + pet: "", + location: "", + pay: "", + start: "", + status: "", + end: "", + }); + } else { + throw new Error("Failed to save Job"); + } + }) + .catch((err) => { + console.error(err); + toast.error("Failed to update Job"); + }); + } + }; + + const onClickCancel = () => { + const cancelConsent = window.confirm("Are you sure you want to discard these changes?"); + if (cancelConsent) { + setJobFormData({ + pet: "", + location: "", + pay: "", + start: "", + status: "", + end: "", + }); + } + }; + + const getCurrentDateTime = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = (now.getMonth() + 1).toString().padStart(2, '0'); + const day = now.getDate().toString().padStart(2, '0'); + const hour = now.getHours().toString().padStart(2, '0'); + const minute = now.getMinutes().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T${hour}:${minute}`; + }; + + const handleStartUpdate = (e: any) => { + if (e.target.value) { + const selectedStart = new Date(e.target.value); + const now = new Date(); + + if (selectedStart > now) { + setJobFormData({ ...jobFormData, start: e.target.value }); + } + else { + setJobFormData({ ...jobFormData, start: "" }); + toast.error("Start datetime must be in the future."); + } + } + }; + + const handleEndUpdate = (e: any) => { + if (e.target.value) { + const startDate = new Date(jobFormData.start); + const selectedEndDate = new Date(e.target.value); + + if (selectedEndDate > startDate) { + setJobFormData({ ...jobFormData, end: e.target.value }); + } + else { + toast.error("End datetime must be after start."); + } + } + }; + + const checkDates = () => { + const startDateTime = new Date(jobFormData.start); + const endDateTime = new Date(jobFormData.end); + const now = new Date(); + + if (startDateTime <= now) { + throw new Error("Start date time must be in the future."); + } + else if (endDateTime <= startDateTime) { + throw new Error("End date time must be after start.") + } + }; + + return ( +
+ + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg" : "inline-block p-4 bg-gray-50 rounded-t-lg hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("view")} + > + View My Jobs + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg ml-1" : "inline-block p-4 bg-gray-50 rounded-t-lg ml-1 hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("add")} + > + Add Job + + + + {activeTab === "view" && } + + {activeTab === "add" && ( +
+ + + + + + + setJobFormData({ ...jobFormData, pay: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + handleStartUpdate(e)} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + + handleEndUpdate(e)} + className="border border-gray-300 rounded-md p-2 mt-1" + /> +
+ )} +
+ + +
+
+
+
+
+ ); +}; + +export default JobPage; diff --git a/frontend/src/Locations.tsx b/frontend/src/Locations.tsx index 4c260ec..e9f2f30 100644 --- a/frontend/src/Locations.tsx +++ b/frontend/src/Locations.tsx @@ -2,12 +2,12 @@ import { PlusCircleIcon, ShieldExclamationIcon } from "@heroicons/react/24/outli import axios from "axios"; import React from "react"; import { useState } from "react"; +import toast from "react-hot-toast"; import { API_ROUTES } from "./constants"; // import fakeData from "./fakeData.json"; import Modal from "./Modal"; import { FurbabyLocation } from "./types"; -import toast from "react-hot-toast"; const Locations = () => { const [open, setOpen] = useState(false); @@ -35,8 +35,7 @@ const Locations = () => { ) .then((response) => { // TODO: handle response - if (response.status === 201) - { + if (response.status === 201) { onCloseModal(); toast.success("Location added successfully."); } @@ -44,7 +43,7 @@ const Locations = () => { }) .catch((err) => { // TODO: handle error - toast.error("Failed to add location."); + toast.error("Failed to add location."); console.error(err); }); }; @@ -72,7 +71,7 @@ const Locations = () => { React.useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - getLocations()//.then((response) => { + getLocations(); //.then((response) => { }, []); const setAsDefault = (location: FurbabyLocation) => { diff --git a/frontend/src/PetProfiles.tsx b/frontend/src/PetProfiles.tsx new file mode 100644 index 0000000..22e770d --- /dev/null +++ b/frontend/src/PetProfiles.tsx @@ -0,0 +1,512 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { Tab } from "@headlessui/react"; +import { API_ROUTES } from "./constants"; +import toast from "react-hot-toast"; + +interface Pet { + id: string; + name: string; + species: string; + color: string; + height: string; + breed: string; + weight: string; + + chip_number: string; + health_requirements: string; +} + +interface EditPetFormData { + name: string; + species: string; + breed: string; + weight: string; + color: string; + height: string; + chip_number: string; + health_requirements: string; + pictures: string[]; +} + +interface PetProfilePageProps { } + +const PetProfiles: React.FC = () => { + const [pets, setPets] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [editingPet, setEditingPet] = useState(null); + const [editFormData, setEditFormData] = useState({ + name: "", + species: "", + breed: "", + weight: "", + color: "", + height: "", + chip_number: "", + health_requirements: "", + }); + + useEffect(() => { + fetchPets(); + }, []); + + const fetchPets = async () => { + try { + const response = await axios.get(`${API_ROUTES.PETS}`); + + if (response.status !== 200) { + throw new Error(`Failed to fetch pets. Status: ${response.status}`); + } + + setPets(response.data); + } catch (error: any) { + console.error("Error fetching pets:", error.message); + setError("Failed to fetch pets. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (petId: string) => { + const deleteConsent = window.confirm("Are you sure you want to delete this pet?"); + if (deleteConsent) { + try { + const response = await axios.delete(`${API_ROUTES.PETS}${petId}`); + + if (response.status === 204) { + setPets((prevPets) => prevPets.filter((pet) => pet.id !== petId)); + toast.success("Pet profile deleted successfully"); + } else { + throw new Error("Failed to delete pet profile"); + } + } catch (err) { + console.error(err); + toast.error("Failed to delete pet profile"); + } + } + }; + + const handleEdit = (pet: Pet) => { + setEditingPet(pet); + setEditFormData({ + name: pet.name, + species: pet.species, + breed: pet.breed, + weight: pet.weight, + color: pet.color, + height: pet.height, + chip_number: pet.chip_number, + health_requirements: pet.health_requirements, + }); + }; + + const handleEditCancel = () => { + setEditingPet(null); + setEditFormData({ + name: "", + species: "", + breed: "", + weight: "", + color: "", + height: "", + chip_number: "", + health_requirements: "", + }); + }; + + const handleEditSave = async (petId: string) => { + const saveConsent = window.confirm("Are you sure you want to save these changes?"); + if (saveConsent) { + try { + const response = await axios.put(`${API_ROUTES.PETS}${petId}/`, { + name: editFormData.name, + species: editFormData.species, + breed: editFormData.breed, + weight: editFormData.weight, + color: editFormData.color, + height: editFormData.height, + chip_number: editFormData.chip_number, + health_requirements: editFormData.health_requirements, + }); + + if (response.status === 200) { + const updatedPetIndex = pets.findIndex((pet) => pet.id === petId); + if (updatedPetIndex !== -1) { + const updatedPets = [...pets]; + updatedPets[updatedPetIndex] = { + ...updatedPets[updatedPetIndex], + name: editFormData.name, + species: editFormData.species, + breed: editFormData.breed, + weight: editFormData.weight, + color: editFormData.color, + height: editFormData.height, + chip_number: editFormData.chip_number, + health_requirements: editFormData.health_requirements, + }; + setPets(updatedPets); + } + + toast.success("Pet profile updated successfully"); + setEditingPet(null); + } else { + throw new Error("Failed to edit pet profile"); + } + } catch (err) { + console.error(err); + toast.error("Failed to edit pet profile"); + } + } + }; + + if (loading) { + return

Loading...

; + } + + return ( +
+ {error &&

{error}

} +
    + {pets.map((pet: Pet) => ( +
  • +
    + {editingPet === pet ? ( +
    +

    Edit Pet Profile

    +
    + + setEditFormData({ ...editFormData, name: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + + setEditFormData({ ...editFormData, species: e.target.value }) + } + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({ ...editFormData, breed: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({...editFormData, color:e.target.value})} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({...editFormData, height:e.target.value})} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({ ...editFormData, weight: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({ ...editFormData, chip_number: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> + + setEditFormData({ ...editFormData, health_requirements: e.target.value })} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + /> +
    +
    + + +
    +
    + ) : ( +
    +

    Name: {pet.name}

    +

    Species: {pet.species}

    +

    Breed: {pet.breed}

    +

    Color: {pet.color}

    +

    Height: {pet.height}

    +

    Weight: {pet.weight}

    +

    Chip Number: {pet.chip_number}

    +

    Health Requirements: {pet.health_requirements}

    +
    + )} +
    + {!editingPet && ( + + )} + +
    +
    +
  • + ))} +
+
+ ); +}; + +const PetProfilePage: React.FC = () => { + const [activeTab, setActiveTab] = useState("view"); + const [petFormData, setPetFormData] = useState({ + name: "", + species: "", + color: "", + height: "", + breed: "", + weight: "", + pictures: ["url1", "url2", "url3"], + chip_number: "", + health_requirements: "", + }); + + const onClickSave = () => { + const saveConsent = window.confirm("Are you sure you want to make these changes?"); + if (saveConsent) { + axios + .post(API_ROUTES.PETS, petFormData) + .then((response) => { + if (response.status === 201) { + toast.success("Pet profile updated successfully"); + setPetFormData({ + name: "", + species: "", + color: "", + height: "", + breed: "", + weight: "", + pictures: ["url1", "url2", "url3"], + chip_number: "", + health_requirements: "", + }); + } else { + throw new Error("Failed to save pet profile"); + } + }) + .catch((err) => { + console.error(err); + toast.error("Failed to update pet profile"); + }); + } + }; + + const onClickCancel = () => { + const cancelConsent = window.confirm("Are you sure you want to discard these changes?"); + if (cancelConsent) { + setPetFormData({ + name: "", + species: "", + color: "", + height: "", + breed: "", + weight: "", + pictures: ["url1", "url2", "url3"], + chip_number: "", + health_requirements: "", + }); + } + }; + + return ( +
+ + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg" : "inline-block p-4 bg-gray-50 rounded-t-lg hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("view")} + > + View Pet Profiles + + + selected ? "inline-block p-4 text-gray-800 bg-gray-300 rounded-t-lg ml-1" : "inline-block p-4 bg-gray-50 rounded-t-lg ml-1 hover:text-gray-600 hover:bg-gray-100 " + } + onClick={() => setActiveTab("add")} + > + Add Pet Profile + + + + {activeTab === "view" && } + + {activeTab === "add" && ( +
+ + setPetFormData({ ...petFormData, name: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + setPetFormData({ ...petFormData, species: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + setPetFormData({ ...petFormData, breed: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + setPetFormData({ ...petFormData, color: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + + setPetFormData({ ...petFormData, height: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + setPetFormData({ ...petFormData, weight: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + setPetFormData({ ...petFormData, chip_number: e.target.value })} + className="border border-gray-300 rounded-md p-2 mt-1" + /> + + + + setPetFormData({ ...petFormData, health_requirements: e.target.value }) + } + className="border border-gray-300 rounded-md p-2 mt-1" + /> +
+ )} +
+ + +
+
+
+
+
+ ); +}; + +export default PetProfilePage; diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx index 7f5ceaa..e26a09d 100644 --- a/frontend/src/auth/AuthProvider.tsx +++ b/frontend/src/auth/AuthProvider.tsx @@ -12,6 +12,7 @@ type UserSession = { email: string; id: string; profilePicture: string; + user_type?: ("sitter" | "owner")[]; }; interface AuthenticationState { @@ -46,6 +47,7 @@ const AuthProvider = ({ children }: React.PropsWithChildren) => { profilePicture: DEFAULT_PROFILE_PICTURE, name: "", email: "", + user_type: [], }, isSessionSet: false, sessionCheckLoading: false, @@ -77,8 +79,7 @@ const AuthProvider = ({ children }: React.PropsWithChildren) => { .then((response) => { if (response.status === 201) { toast.success( - `An account has been created for ${ - response.data?.data?.email ?? "" + `An account has been created for ${response.data?.data?.email ?? "" }. Redirecting to login page...` ); navigate("/login"); @@ -125,6 +126,10 @@ const AuthProvider = ({ children }: React.PropsWithChildren) => { navigate(ROUTES.PROTECTED_ROUTES.HOME); } }) + .then(() => { + handleSession(); + window.location.reload(); + }) .catch((error) => { notify({ title: "Failed to login", @@ -151,6 +156,13 @@ const AuthProvider = ({ children }: React.PropsWithChildren) => { ...prevState, isSessionSet: false, sessionCheckLoading: false, + sessionInformation: { + id: "", + profilePicture: DEFAULT_PROFILE_PICTURE, + name: "", + email: "", + user_type: [], + }, })); toast.success("logged out successfully"); navigate(ROUTES.LOGIN); diff --git a/frontend/src/constants.tsx b/frontend/src/constants.tsx index 14dbaa1..71044ba 100644 --- a/frontend/src/constants.tsx +++ b/frontend/src/constants.tsx @@ -30,6 +30,9 @@ export const ROUTES = { HOME: "/home", PROFILE: "/profile", SETTINGS: "/settings", + PET_PROFILES: "/pet-profiles", + JOBS: "/jobs", + DASHBOARD: "/home", LOCATIONS: "/locations", }, } as const; @@ -53,4 +56,7 @@ export const API_ROUTES = { PROFILE_PICTURE: "api/user/profile_picture", LOCATION: "api/user/locations", }, + PETS: "pets/", + JOBS: "jobs/", + APPLY: "applications/", } as const; diff --git a/furbaby/api/serializers.py b/furbaby/api/serializers.py index a9c28eb..8c00b57 100644 --- a/furbaby/api/serializers.py +++ b/furbaby/api/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Users, Locations, Pets +from .models import Users, Locations, Pets, Jobs, Applications from django.contrib.auth.hashers import make_password from django.core.exceptions import ValidationError @@ -103,49 +103,31 @@ class Meta: exclude = () -class UserLocationSerializer(serializers.Serializer): +class JobSerializer(serializers.ModelSerializer): user = serializers.HiddenField(default=serializers.CurrentUserDefault()) - address = serializers.CharField(max_length=200) - city = serializers.CharField(max_length=100) - country = serializers.CharField(max_length=100) - zipcode = serializers.CharField(max_length=20) - default_location = serializers.BooleanField() class Meta: - model = Locations - fields = [ - "user_id", - "address", - "city", - "country", - "zipcode", - "default_location", - ] + model = Jobs + fields = "__all__" - # TODO: There should be only one default address per user - def validate(self, data): - city = data.get("city", "") - city = str(city).lower() - country = data.get("country", "") - country = str(country).lower() - cities_allowed_list = ["new york city", "nyc"] - countries_allowed_list = [ - "united states", - "usa", - "us", - "america", - "united states of america", - ] +class UserSerializer(serializers.Serializer): + id = serializers.UUIDField() + username = serializers.CharField() + date_of_birth = serializers.DateField() + experience = serializers.CharField() + qualifications = serializers.CharField() - if city not in cities_allowed_list: - raise ValidationError("Users must be located in New York City/NYC") - if country not in countries_allowed_list: - raise ValidationError("Users must be located in the United States of America/USA") - if data.get("default_location", True): - # If the user is setting a default location, set all other locations to false - Locations.objects.filter(user_id=data.get("user").id).update(default_location=False) - return data - def create(self, validated_data): - return Locations.objects.create(**validated_data) +class ApplicationSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Applications + fields = "__all__" + + def to_representation(self, instance): + representation = super().to_representation(instance) + user_representation = UserSerializer(instance.user).data + representation["user"] = user_representation + return representation diff --git a/furbaby/api/urls.py b/furbaby/api/urls.py index d0b97b1..bbb5337 100644 --- a/furbaby/api/urls.py +++ b/furbaby/api/urls.py @@ -6,6 +6,8 @@ UserLoginView, PetListCreateView, PetRetrieveUpdateDeleteView, + JobView, + ApplicationView, ) # NOTE: We might have to use the decorator csrf_protect to ensure that @@ -44,4 +46,6 @@ PetRetrieveUpdateDeleteView.as_view(), name="pet-retrieve-update-delete", ), + path("jobs/", JobView.as_view(), name="custom-job-view"), + path("applications/", ApplicationView.as_view(), name="application-list"), ] diff --git a/furbaby/api/views.py b/furbaby/api/views.py index 436c001..9bbd028 100644 --- a/furbaby/api/views.py +++ b/furbaby/api/views.py @@ -1,34 +1,39 @@ import json import os +from datetime import datetime, timezone, timedelta + from django.http import JsonResponse, HttpResponse from django.contrib.auth import login, logout from drf_standardized_errors.handler import exception_handler from rest_framework import status from rest_framework.views import APIView -from rest_framework.generics import GenericAPIView - -# from django.views.decorators.csrf import ensure_csrf_cookie -from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView +from rest_framework.generics import GenericAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView from rest_framework.permissions import IsAuthenticated from rest_framework.exceptions import PermissionDenied from .utils import json_response, make_s3_path from django.conf import settings from rest_framework.decorators import api_view +from rest_framework.response import Response -from .models import Pets, Users, Locations +from .models import Locations +from api.auth_backends import EmailBackend +from .models import Pets, Users, Locations, Jobs, Applications +from .utils import json_response from api.auth_backends import EmailBackend from .serializers import ( RegistrationSerializer, UserLocationSerializer, UserLoginSerializer, PetSerializer, + JobSerializer, + ApplicationSerializer, ) from django.core.mail import EmailMultiAlternatives from django.dispatch import receiver from django.template.loader import render_to_string - from django_rest_passwordreset.signals import reset_password_token_created +from django.core.exceptions import ValidationError from django.views.decorators.csrf import csrf_protect from django.core.serializers import serialize @@ -42,6 +47,17 @@ s3BucketName = getattr(settings, "AWS_BUCKET_NAME") +def update_job_status(job): + applications_count = Applications.objects.filter(job=job).count() + if applications_count < 10: + job.status = "open" + elif applications_count > 10: + job.status = "job_acceptance_pending" + else: + job.status = "acceptance_complete" + job.save() + + class UserRegistrationView(GenericAPIView): # the next line is to disable CORS for that endpoint/view authentication_classes = [] @@ -124,6 +140,7 @@ def session_view(request): user_response_body = { "id": current_user.id, "email": current_user.email.lower(), + "user_type": current_user.user_type, } if current_user.first_name != None or current_user.last_name != None: @@ -521,6 +538,38 @@ def delete_location_record(self, request): ) +@api_view(["GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"]) +def user_location_view(request): + location_view = UserLocationView() + + if not request.user.is_authenticated: + return json_response({"isAuthenticated": False}, status=status.HTTP_401_UNAUTHORIZED) + + # fetch all user locations for the user + if request.method == "GET": + locations_list = location_view.get_user_locations(request) + return json_response( + locations_list, status=status.HTTP_200_OK, safe=False, include_data=False + ) + + # insert a new location record for the user + if request.method in ["POST"]: + return location_view.insert_location_record(request) + + # update a location record for the user + if request.method in ["PUT", "PATCH"]: + return location_view.update_location_record(request) + + # delete a location record for the user + if request.method == "DELETE": + return location_view.delete_location_record(request) + + return json_response( + {"error": "incorrect request method supplied"}, + status=status.HTTP_405_METHOD_NOT_ALLOWED, + ) + + def __get_user_pet_picture__(request): pet_id = request.data["pet_id"] @@ -679,182 +728,233 @@ class PetRetrieveUpdateDeleteView(RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticated] -# This class is for the user location(s) -class UserLocationView(APIView): - # Fetch the locations serializer - serializer_class = UserLocationSerializer - - def get_exception_handler(self): - return exception_handler - - # takes as input the user id, request and inserts a new location record for the user - def insert_location_record(self, request): - serializer = self.serializer_class(data=request.data, context={"request": request}) - - if not serializer.is_valid(): - return json_response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - instance = serializer.save() +class JobView(APIView): + permission_classes = [IsAuthenticated] - return json_response( - instance.id, status=status.HTTP_201_CREATED, include_data=False, safe=False + def job_status_check(self): + to_be_cancelled_queryset = Jobs.objects.filter(status="open").filter( + start__lte=datetime.now(timezone.utc) - timedelta(hours=5) ) + for job in to_be_cancelled_queryset: + if job.status == "open": + job.status = "cancelled" + job.save() + return - # takes as input a location object and returns a dictionary of the location key value pairs - def get_location_record(self, location=None): - if location == None: - return None - return { - "id": location.id, - "address": location.address, - "city": location.city, - "country": location.country, - "zipcode": location.zipcode, - "user_id": location.user_id, - "default_location": location.default_location, - } + def get_all(self): + self.job_status_check() + return Jobs.objects.filter(status="open") - # takes as input a user_id and returns a JSON of all the locations for that user - def get_user_locations(self, request): - locations = Locations.objects.filter(user_id=request.user.id) - location_list = [ - { - "id": location.id, - "address": location.address, - "city": location.city, - "country": location.country, - "zipcode": location.zipcode, - "default_location": location.default_location, - } - for location in locations - ] - return location_list - - # takes as input a location_id and location fields and updates the location record - def update_location_record(self, request): - location_id: str = request.data["id"] - - if location_id == None: - return json_response( - {"message": "location_id is missing in request"}, - status=status.HTTP_404_NOT_FOUND, - ) + def get_queryset(self): + self.job_status_check() + return Jobs.objects.filter(user_id=self.request.user.id) + def get_object(self, job_id): try: - updated_fields = [] - location = Locations.objects.get(id=location_id) - if "address" in request.data: - location.address = request.data["address"] - updated_fields.append("address") - if "city" in request.data: - location.city = request.data["city"] - updated_fields.append("city") - if "country" in request.data: - location.country = request.data["country"] - updated_fields.append("country") - if "zipcode" in request.data: - location.zipcode = request.data["zipcode"] - updated_fields.append("zipcode") - if "default_location" in request.data: - if request.data["default_location"] == True: - # unset all other locations as default - Locations.objects.filter(user_id=request.user.id).update(default_location=False) - location.default_location = request.data["default_location"] - updated_fields.append("default_location") - - # location.save() - location.save(update_fields=updated_fields) - - return json_response( - self.get_location_record(location), - status.HTTP_200_OK, - safe=False, - include_data=False, - ) - except Locations.DoesNotExist: - return json_response( - data={ - "error": "location not found for user", - "location id": location_id, - "user id": request.user.id, - }, - status=status.HTTP_404_NOT_FOUND, - ) - except Exception as e: - return json_response( - data={ - "error": "something went wrong while updating the location record", - "error message": str(e), - "location id": location_id, - "user id": request.user.id, - }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) + if "sitter" in self.request.user.user_type: + return Jobs.objects.get(id=job_id) + else: + return Jobs.objects.get(id=job_id, user=self.request.user) - def delete_location_record(self, request): - location_id: str = request.data["id"] - - if location_id == None: - return json_response( - {"message": "location_id is missing in request"}, - status=status.HTTP_404_NOT_FOUND, - ) + except Jobs.DoesNotExist: + raise ValidationError("Job not found or you do not have permission to access this job.") + def get(self, request, *args, **kwargs): + job_id = request.query_params.get("id") + if job_id: + job = self.get_object(job_id) + serializer = JobSerializer(job) + return JsonResponse(serializer.data) + else: + print(request.user.user_type) + if "owner" in request.user.user_type and "sitter" in request.user.user_type: + queryset_owner = self.get_queryset() + queryset_sitter = self.get_all() + + serializer_owner = JobSerializer(queryset_owner, many=True) + serializer_sitter = JobSerializer(queryset_sitter, many=True) + + response_data = { + "owner_jobs": serializer_owner.data, + "sitter_jobs": serializer_sitter.data, + } + print(response_data) + return JsonResponse(response_data, safe=False) + + elif "owner" in request.user.user_type: + queryset_owner = self.get_queryset() + serializer_owner = JobSerializer(queryset_owner, many=True) + response_data = { + "owner_jobs": serializer_owner.data, + } + return JsonResponse(response_data, safe=False) + + elif "sitter" in request.user.user_type: + print("here") + queryset_sitter = self.get_all() + serializer_sitter = JobSerializer(queryset_sitter, many=True) + response_data = { + "sitter_jobs": serializer_sitter.data, + } + return JsonResponse(response_data, safe=False) + + def put(self, request, *args, **kwargs): + # Retrieve the application ID from the URL or request data + job_id = self.request.data.get("id") try: - location = Locations.objects.get(id=location_id) - location.delete() - return json_response( - {"message": "location deleted successfully"}, - status.HTTP_200_OK, - ) - except Locations.DoesNotExist: - return json_response( - data={ - "error": "location not found for user", - "location id": location_id, - "user id": request.user.id, - }, - status=status.HTTP_404_NOT_FOUND, - ) + job = Jobs.objects.get(id=job_id) + print(job.status) + if job.status == "open": + job.status = "acceptance_complete" + job.save() + return Response({"detail": "Job status updated successfully."}) + + except Jobs.DoesNotExist: + return Response({"detail": "Job not found."}, status=404) except Exception as e: - return json_response( - data={ - "error": "something went wrong while deleting the location record", - "error message": str(e), - "location id": location_id, - "user id": request.user.id, - }, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) + return Response({"detail": str(e)}, status=500) + def post(self, request, *args, **kwargs): + request.data["user_id"] = request.user.id + if "owner" in request.user.user_type: + pet_id = self.request.data.get("pet") + try: + pet = Pets.objects.get(id=pet_id, owner=self.request.user) + except Pets.DoesNotExist: + raise ValidationError("Invalid pet ID or you do not own the pet.") + + # Ensure that the pet owner is the one creating the job + if pet.owner != self.request.user: + raise PermissionDenied("You do not have permission to create a job for this pet.") + # Now, create the job with the specified pet + serializer = JobSerializer(data=request.data, context={"request": request}) + serializer.is_valid(raise_exception=True) + serializer.save(user=self.request.user, pet=pet) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + raise PermissionDenied("You are not allowed to create a job.") -@api_view(["GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"]) -def user_location_view(request): - location_view = UserLocationView() + def delete(self, request, *args, **kwargs): + job_id = self.request.data.get("id") + if job_id: + job = self.get_object(job_id) + job.delete() + return JsonResponse({"detail": "Job deleted successfully."}) - if not request.user.is_authenticated: - return json_response({"isAuthenticated": False}, status=status.HTTP_401_UNAUTHORIZED) - # fetch all user locations for the user - if request.method == "GET": - locations_list = location_view.get_user_locations(request) - return json_response( - locations_list, status=status.HTTP_200_OK, safe=False, include_data=False - ) +class ApplicationView(APIView): + permission_classes = [IsAuthenticated] - # insert a new location record for the user - if request.method in ["POST"]: - return location_view.insert_location_record(request) + def get(self, request, *args, **kwargs): + job_id = request.query_params.get("job_id") - # update a location record for the user - if request.method in ["PUT", "PATCH"]: - return location_view.update_location_record(request) + if job_id: + applications = Applications.objects.filter(job_id=job_id) + serializer = ApplicationSerializer(applications, many=True) + return JsonResponse(serializer.data, safe=False) - # delete a location record for the user - if request.method == "DELETE": - return location_view.delete_location_record(request) + else: + user_id = request.user.id + applications = Applications.objects.filter(user_id=user_id) + serializer = ApplicationSerializer(applications, many=True) + return JsonResponse(serializer.data, safe=False) + + def put(self, request, *args, **kwargs): + # Retrieve the application ID from the URL or request data + application_id = request.data.get("id") + print(application_id) + try: + application = Applications.objects.get(id=application_id) + except Applications.DoesNotExist: + raise ValidationError("Application not found.") + job_instance = Jobs.objects.get(id=application.job_id) + + # Check if the user making the request is the owner of the application + if request.user == job_instance.user: + print(request.user) + # Check if the job status is "open" + if job_instance.status == "open": + # Update the application status based on your requirements + new_status = request.data.get("status") + if new_status: + application.status = new_status + application.save() + + # You can perform additional actions based on the new status if needed + # For example, update the job status or send notifications + + return Response( + {"detail": "Application status updated successfully."}, + status=status.HTTP_200_OK, + ) + else: + return Response( + {"detail": "New status is required for the update."}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": "The job status must be 'open' to update the application."}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": "You do not have permission to update this application."}, + status=status.HTTP_403_FORBIDDEN, + ) - return json_response( - {"error": "incorrect request method supplied"}, - status=status.HTTP_405_METHOD_NOT_ALLOWED, - ) + def post(self, request, *args, **kwargs): + job_id = self.request.data.get("id") + print(request.data) + print(job_id) + try: + job = Jobs.objects.get(id=job_id) + except Jobs.DoesNotExist: + raise ValidationError("Job not found.") + + # Check if the user is allowed to apply for this job + if "sitter" in request.user.user_type: + # Check if the job is still available (you can add more checks based on your logic) + if job.status == "open": + # Check if the user is not the owner of the job + if job.user != request.user: + # Check if the user has not already applied for this job + if not Applications.objects.filter(user=request.user, job=job).exists(): + # Create an application for the job + application_data = { + "user": request.user.id, + "job": job_id, + "status": "rejected", # You can set an initial status here + "details": {}, # You can add more details if needed + } + application_serializer = ApplicationSerializer( + data=application_data, context={"request": self.request} + ) + application_serializer.is_valid(raise_exception=True) + application_serializer.save() + update_job_status(job) + + return Response( + {"detail": "Application submitted successfully."}, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + {"detail": "You have already applied for this job."}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": "You cannot apply to your own job."}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": "This job is no longer available."}, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + return Response( + {"detail": "Only pet sitters can apply for jobs."}, status=status.HTTP_403_FORBIDDEN + )