diff --git a/src/App.js b/src/App.js index 1648d71..a326e7b 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import JobApplicationList from './components/JobApplicationList'; import CssBaseline from '@mui/material/CssBaseline'; import { createTheme, ThemeProvider } from '@mui/material/styles'; -import PrimarySearchAppBar from './components/PrimarySearchAppBar'; +import AppHeader from './components/AppHeader'; import { Route, Routes } from "react-router-dom" const theme = createTheme(); @@ -11,7 +11,7 @@ function App() { return ( - +
diff --git a/src/App.test.js b/src/App.test.js index f00edfe..8871bb0 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,18 +1,18 @@ import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import App from './App'; -import PrimarySearchAppBar from './components/PrimarySearchAppBar'; +import AppHeader from './components/AppHeader'; import JobApplicationList from './components/JobApplicationList'; -jest.mock('./components/PrimarySearchAppBar') +jest.mock('./components/AppHeader') jest.mock('./components/JobApplicationList') it('Should render Job Application list on default route', async () => { - PrimarySearchAppBar.mockImplementation(() =>
PrimarySearchAppBarMock
) + AppHeader.mockImplementation(() =>
AppHeaderMock
) JobApplicationList.mockImplementation(() =>
JobApplicationListrMock
) render( ); - expect(screen.getByText("PrimarySearchAppBarMock")).toBeInTheDocument(); + expect(screen.getByText("AppHeaderMock")).toBeInTheDocument(); }); \ No newline at end of file diff --git a/src/components/PrimarySearchAppBar.jsx b/src/components/AppHeader.jsx similarity index 74% rename from src/components/PrimarySearchAppBar.jsx rename to src/components/AppHeader.jsx index e829ef0..19335b2 100644 --- a/src/components/PrimarySearchAppBar.jsx +++ b/src/components/AppHeader.jsx @@ -7,19 +7,20 @@ import IconButton from '@mui/material/IconButton'; import Badge from '@mui/material/Badge'; import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import AddCircleIcon from '@mui/icons-material/AddCircle'; -import MailIcon from '@mui/icons-material/Mail'; import NotificationsIcon from '@mui/icons-material/Notifications'; +import EventAvailableIcon from '@mui/icons-material/EventAvailable'; import AccountCircle from '@mui/icons-material/AccountCircle'; import MoreIcon from '@mui/icons-material/MoreVert'; import { useQuery } from '@apollo/client'; -import { GET_PROFILE } from '../graphql/query'; +import { GET_PROFILE, GET_ALL_OFFERS, GET_ALL_INTERVIEWS } from '../graphql/query'; import SearchBar from './SearchBar'; import JobApplicationDialog from './JobApplicationDialog'; import ProfileDialog from './ProfileDialog'; import JobApplicationList from './JobApplicationList'; import MobileMenu from './MobileMenu'; +import dayjs from 'dayjs'; -export default function PrimarySearchAppBar() { +export default function AppHeader() { const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = useState(null); const [jobApplicationOpen, setJobApplicationOpen] = useState(false); const [profileOpen, setProfileOpen] = useState(false); @@ -33,6 +34,31 @@ export default function PrimarySearchAppBar() { variables: { id }, }); + const { data: offersData, loading: offersLoading, error: offersError } = useQuery(GET_ALL_OFFERS, { + fetchPolicy: 'network-only', + }); + + const { data: interviewsData, loading: interviewsLoading, error: interviewsError } = useQuery(GET_ALL_INTERVIEWS, { + fetchPolicy: 'network-only', + }); + + const offerCount = offersData?.allOffer?.length || 0; + + const filteredInterviews = interviewsData?.allInterview?.filter((interview) => { + const interviewDate = dayjs(interview.interviewDate); + if (!interviewDate.isValid()) { + console.warn('Invalid dayjs object for interviewDate:', interview.interviewDate); + return false; + } + + return ( + (interviewDate.isSame(dayjs(), 'day') || interviewDate.isAfter(dayjs(), 'day')) && + ['open', 'pending'].includes(interview.status) + ); + }) || []; + + const interviewCount = filteredInterviews?.length || 0; + const handleProfileMenuOpen = () => { setProfileOpen(true); setProfile(data.profileById); @@ -88,13 +114,13 @@ export default function PrimarySearchAppBar() { - - - + + + - - + + @@ -129,6 +155,8 @@ export default function PrimarySearchAppBar() { handleMobileMenuClose={handleMobileMenuClose} handleProfileMenuOpen={handleProfileMenuOpen} handleJobApplicationOpen={handleJobApplicationOpen} + interviewCount={interviewCount} + offerCount={offerCount} /> diff --git a/src/components/MobileMenu.jsx b/src/components/MobileMenu.jsx index 7ee71bc..deb5447 100644 --- a/src/components/MobileMenu.jsx +++ b/src/components/MobileMenu.jsx @@ -4,11 +4,11 @@ import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Badge from '@mui/material/Badge'; import AddCircleIcon from '@mui/icons-material/AddCircle'; -import MailIcon from '@mui/icons-material/Mail'; +import EventAvailableIcon from '@mui/icons-material/EventAvailable'; import NotificationsIcon from '@mui/icons-material/Notifications'; import AccountCircle from '@mui/icons-material/AccountCircle'; -export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handleMobileMenuClose, handleProfileMenuOpen, handleJobApplicationOpen }) { +export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handleMobileMenuClose, handleProfileMenuOpen, handleJobApplicationOpen, interviewCount, offerCount }) { const mobileMenuId = 'primary-search-account-menu-mobile'; return ( New

- - - + + + -

Offers

+

Interviews

- - + + -

Interviews

+

Offers

diff --git a/src/components/forms/InterviewsForm.jsx b/src/components/forms/InterviewsForm.jsx index 8069d15..f461833 100644 --- a/src/components/forms/InterviewsForm.jsx +++ b/src/components/forms/InterviewsForm.jsx @@ -14,7 +14,7 @@ import ConfirmDialog from '../common/ConfirmDialog'; import SnackbarComponent from '../common/SnackbarComponent'; import useSnackbar from '../hooks/useSnackbar'; import { ADD_INTERVIEW, UPDATE_INTERVIEW, DELETE_INTERVIEW } from '../../graphql/mutation'; -import { GET_INTERVIEWS_BY_JOB_APPLICATION_ID } from '../../graphql/query'; +import { GET_INTERVIEWS_BY_JOB_APPLICATION_ID, GET_ALL_INTERVIEWS } from '../../graphql/query'; import dayjs from 'dayjs'; import { Grid } from '@mui/material'; @@ -30,15 +30,24 @@ export default function InterviewsForm({ jobApplicationId }) { }); const [addInterview] = useMutation(ADD_INTERVIEW, { - refetchQueries: [{ query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }], + refetchQueries: [ + { query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }, + { query: GET_ALL_INTERVIEWS } + ], }); const [updateInterview] = useMutation(UPDATE_INTERVIEW, { - refetchQueries: [{ query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }], + refetchQueries: [ + { query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }, + { query: GET_ALL_INTERVIEWS } + ], }); const [deleteInterview] = useMutation(DELETE_INTERVIEW, { - refetchQueries: [{ query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }], + refetchQueries: [ + { query: GET_INTERVIEWS_BY_JOB_APPLICATION_ID, variables: { jobApplicationId } }, + { query: GET_ALL_INTERVIEWS } + ], onError: (err) => { console.error('Error deleting interview:', err.message); showSnackbar('Error deleting interview', 'error'); diff --git a/src/components/forms/OfferForm.jsx b/src/components/forms/OfferForm.jsx index 3a4d690..12c616c 100644 --- a/src/components/forms/OfferForm.jsx +++ b/src/components/forms/OfferForm.jsx @@ -13,7 +13,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { ADD_OFFER, UPDATE_OFFER, DELETE_OFFER } from '../../graphql/mutation'; -import { GET_OFFER } from '../../graphql/query'; +import { GET_OFFER, GET_ALL_OFFERS } from '../../graphql/query'; import dayjs from 'dayjs'; import { Grid } from '@mui/material'; @@ -43,7 +43,10 @@ export default function OfferForm({ jobApplicationId, handleClose }) { }); const [deleteOffer, { loading: deleteOfferLoading }] = useMutation(DELETE_OFFER, { - refetchQueries: [{ query: GET_OFFER, variables: { jobApplicationId } }], + refetchQueries: [ + { query: GET_OFFER, variables: { jobApplicationId } }, + { query: GET_ALL_OFFERS }, + ], }); useEffect(() => { @@ -100,6 +103,7 @@ export default function OfferForm({ jobApplicationId, handleClose }) { description: offerData.description, offerDate: offerData.offerDate, }, + refetchQueries: [{ query: GET_ALL_OFFERS }], }); showSnackbar('Offer added successfully!', 'success'); } diff --git a/src/graphql/query.js b/src/graphql/query.js index 3faeb74..9ed52cf 100644 --- a/src/graphql/query.js +++ b/src/graphql/query.js @@ -65,6 +65,19 @@ export const GET_OFFER = gql` ` ; +export const GET_ALL_OFFERS = gql` + query GetOffers { + allOffer { + id + jobApplicationId + offerDate + salaryOffered + description + } + } +` +; + export const GET_ALL_INTERVIEWS = gql` query GetAllInterviews { allInterview {