diff --git a/src/components/AppHeader.jsx b/src/components/AppHeader.jsx index c954218..0c6b06c 100644 --- a/src/components/AppHeader.jsx +++ b/src/components/AppHeader.jsx @@ -11,33 +11,45 @@ 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 { Tooltip } from '@mui/material'; import { useQuery } from '@apollo/client'; import { GET_PROFILE, GET_ALL_OFFERS, GET_ALL_INTERVIEWS } from '../graphql/query'; import SearchBar from './SearchBar'; +import InterviewListDialog from './InterviewListDialog'; import JobApplicationDialog from './JobApplicationDialog'; import OfferListDialog from './OfferListDialog'; +import useInterviewListDialog from './hooks/useInterviewDialog'; import useJobApplicationDialog from './hooks/useJobApplicationDialog'; -import useOfferListDialog from './hooks/useOfferList'; +import useOfferListDialog from './hooks/useOfferListDialog'; import ProfileDialog from './ProfileDialog'; import JobApplicationList from './JobApplicationList'; import MobileMenu from './MobileMenu'; -import dayjs from 'dayjs'; +import { getFilteredInterviews } from '../utils/interviewUtil'; export default function AppHeader() { const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = useState(null); const [profileOpen, setProfileOpen] = useState(false); const [profile, setProfile] = useState(''); const [searchTerm, setSearchTerm] = useState(''); - const { data: offersData, loading: offersLoading, error: offersError, refetch } = useQuery(GET_ALL_OFFERS, { + const { data: offersData, loading: offersLoading, error: offersError, refetch: refetchOffers } = useQuery(GET_ALL_OFFERS, { fetchPolicy: 'network-only', }); - const { open, handleOpen, handleClose } = useJobApplicationDialog(refetch); + const { data: interviewsData, loading: interviewsLoading, error: interviewsError, refetch: refetchInterviews } = useQuery(GET_ALL_INTERVIEWS, { + fetchPolicy: 'network-only', + }); + const { open, handleOpen, handleClose } = useJobApplicationDialog(refetchOffers); - const { + const { offerListingDialogOpen, handleOfferListDialogOpen, handleOfferListDialogClose, - } = useOfferListDialog(refetch); + } = useOfferListDialog(refetchOffers); + + const { + interviewListingDialogOpen, + handleInterviewListDialogOpen, + handleInterviewListDialogClose, + } = useInterviewListDialog(refetchInterviews); const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); @@ -48,24 +60,9 @@ export default function AppHeader() { variables: { id }, }); - 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 filteredInterviews = getFilteredInterviews(interviewsData?.allInterview); const interviewCount = filteredInterviews?.length || 0; @@ -96,6 +93,11 @@ export default function AppHeader() { handleClose={handleProfileClose} setOpen={setProfileOpen} /> + - - - - refetch()}> - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + setInterviews(getFilteredInterviews(data.allInterview)), + }); + + const { + open: jobDialogOpen, + jobApplication, + handleOpen: handleJobDialogOpen, + handleClose: handleJobDialogClose + } = useJobApplicationDialog(() => { + refetch().then(({ data }) => setInterviews(getFilteredInterviews(data.allInterview))); + }); + + const columns = [ + { + key: 'jobApplication.companyName', // Nested property example + label: 'Company Name', + sortable: true, + render: (value, row) => value || row.jobApplication?.companyName || 'N/A', // Fallback for nested value + }, + { key: 'interviewDate', label: 'Interview Date', sortable: true }, + { key: 'interviewer', label: 'Interviewer', sortable: true }, + { key: 'description', label: 'Note' }, + { key: 'status', label: 'Status', sortable: true }, + { + key: 'actions', + label: 'Job Application', + render: (value, row) => ( + + ), + align: 'center', + }, + ]; + + const { sortedData, handleSort, getSortIndicator } = useSortableTable(interviews, columns); + + useEffect(() => { + if (open) { + refetch() + .then(({ data }) => { + if (data) setInterviews(getFilteredInterviews(data.allInterview)); + }) + .catch((err) => console.error('Refetch error:', err)); + } + }, [open, refetch]); + + return ( + + + + {loading ? ( + + + + ) : error ? ( + + Something went wrong. Please try again later. + + ) : ( + + )} + + + + + + + ); +} diff --git a/src/components/JobApplicationDialog.jsx b/src/components/JobApplicationDialog.jsx index 417eb97..1dfff9d 100644 --- a/src/components/JobApplicationDialog.jsx +++ b/src/components/JobApplicationDialog.jsx @@ -35,6 +35,7 @@ export default function JobApplicationDialog({ jobApplication, handleClose, open }} > + {!isNew && ( + )} {activeTab === 0 && ( )} diff --git a/src/components/JobApplicationList.jsx b/src/components/JobApplicationList.jsx index 8d16998..e5501ff 100644 --- a/src/components/JobApplicationList.jsx +++ b/src/components/JobApplicationList.jsx @@ -29,14 +29,12 @@ import { DELETE_JOB_APPLICATION } from '../graphql/mutation'; import { GET_JOB_APPLICATIONS } from '../graphql/query'; export default function JobApplicationList({ searchTerm }) { - // const [open, setOpen] = useState(false); - // const [jobApplication, setJobApplication] = useState(null); const [jobApplicationToDelete, setJobApplicationToDelete] = useState(null); const [localData, setLocalData] = useState([]); const { data, loading, error } = useJobApplications(); const { snackbarOpen, snackbarMessage, showSnackbar, handleSnackbarClose } = useSnackbar(); - const { confirmOpen, openConfirmDialog, cancel } = useConfirmDialog(); + const { confirmDialogOpen, handleConfirmDialogOpen, handleConfirmDialogClose } = useConfirmDialog(); const { open, jobApplication, handleOpen, handleClose } = useJobApplicationDialog(); const [deleteJobApplication] = useMutation(DELETE_JOB_APPLICATION, { @@ -49,13 +47,6 @@ export default function JobApplicationList({ searchTerm }) { } }, [data]); - // const handleOpen = (jobApplication) => { - // setJobApplication(jobApplication); - // setOpen(true); - // }; - - // const handleClose = () => setOpen(false); - const confirmDeleteJobApplication = () => { if (jobApplicationToDelete) { deleteJobApplication({ @@ -69,13 +60,13 @@ export default function JobApplicationList({ searchTerm }) { showSnackbar("Failed to delete the job application."); }); } - cancel(); + handleConfirmDialogClose(); setJobApplicationToDelete(null); }; const handleDeleteJobApplication = (jobApplication) => { setJobApplicationToDelete(jobApplication); - openConfirmDialog(); + handleConfirmDialogOpen(); }; const containsIgnoreCase = (str, searchTerm) => @@ -176,8 +167,8 @@ export default function JobApplicationList({ searchTerm }) { )}

New

- + diff --git a/src/components/__tests__/JobApplicationList.test.jsx b/src/components/__tests__/JobApplicationList.test.jsx index d23f360..4de8535 100644 --- a/src/components/__tests__/JobApplicationList.test.jsx +++ b/src/components/__tests__/JobApplicationList.test.jsx @@ -100,9 +100,9 @@ beforeEach(() => { handleSnackbarClose: jest.fn(), }); require('../hooks/useConfirmDialog').default.mockReturnValue({ - confirmOpen: true, - openConfirmDialog: jest.fn(), - cancel: jest.fn(), + confirmDialogOpen: true, + handleConfirmDialogOpen: jest.fn(), + handleConfirmDialogClose: jest.fn(), }); }); @@ -149,6 +149,7 @@ describe('JobApplicationList', () => { // Simulate clicking the delete button for the first job application const deleteButton = screen.getAllByText('Delete')[0]; + console.log("zeon check: {}", deleteButton) userEvent.click(deleteButton); console.log("first check: {}", screen.getAllByText('Delete')) diff --git a/src/components/common/SortableTable.jsx b/src/components/common/SortableTable.jsx index 172f38e..30494a4 100644 --- a/src/components/common/SortableTable.jsx +++ b/src/components/common/SortableTable.jsx @@ -26,8 +26,8 @@ const SortableTable = ({ data, columns, handleSort, getSortIndicator }) => { > {column.sortable ? ( handleSort(column.key)} > {column.label} diff --git a/src/components/hooks/useConfirmDialog.js b/src/components/hooks/useConfirmDialog.js index 1fe903d..9ae238a 100644 --- a/src/components/hooks/useConfirmDialog.js +++ b/src/components/hooks/useConfirmDialog.js @@ -1,11 +1,11 @@ -import { useState } from 'react'; +import useDialog from './useDialog'; export default function useConfirmDialog() { - const [confirmOpen, setConfirmOpen] = useState(false); + const { dialogOpen, handleOpen, handleClose } = useDialog(); - const openConfirmDialog = () => setConfirmOpen(true); - const confirm = () => setConfirmOpen(false); - const cancel = () => setConfirmOpen(false); - - return { confirmOpen, openConfirmDialog, confirm, cancel }; + return { + confirmDialogOpen: dialogOpen, + handleConfirmDialogOpen: handleOpen, + handleConfirmDialogClose: handleClose, + }; } diff --git a/src/components/hooks/useDialog.js b/src/components/hooks/useDialog.js new file mode 100644 index 0000000..d9ec717 --- /dev/null +++ b/src/components/hooks/useDialog.js @@ -0,0 +1,21 @@ +import { useState } from 'react'; + +export default function useDialog(refetch) { + const [dialogOpen, setDialogOpen] = useState(false); + + const handleOpen = () => { + setDialogOpen(true); + if (refetch) { + refetch(); + } + }; + + const handleClose = () => { + setDialogOpen(false); + if (refetch) { + refetch(); + } + }; + + return { dialogOpen, handleOpen, handleClose }; +} diff --git a/src/components/hooks/useInterviewDialog.js b/src/components/hooks/useInterviewDialog.js new file mode 100644 index 0000000..9d1ba49 --- /dev/null +++ b/src/components/hooks/useInterviewDialog.js @@ -0,0 +1,11 @@ +import useDialog from './useDialog'; + +export default function useInterviewListDialog(refetch) { + const { dialogOpen, handleOpen, handleClose } = useDialog(refetch); + + return { + interviewListingDialogOpen: dialogOpen, + handleInterviewListDialogOpen: handleOpen, + handleInterviewListDialogClose: handleClose, + }; +} diff --git a/src/components/hooks/useOfferList.js b/src/components/hooks/useOfferList.js deleted file mode 100644 index fee84ee..0000000 --- a/src/components/hooks/useOfferList.js +++ /dev/null @@ -1,23 +0,0 @@ -import { useState } from 'react'; - -export default function useOfferListDialog(refetch) { - const [offerListingDialogOpen, setOfferListDialogOpen] = useState(false); - const handleOfferListDialogOpen = () => { - setOfferListDialogOpen(true); - if (refetch) { - refetch(); - } - } - const handleOfferListDialogClose = () => { - setOfferListDialogOpen(false); - if (refetch) { - refetch(); - } - } - - return { - offerListingDialogOpen, - handleOfferListDialogOpen, - handleOfferListDialogClose, - }; -} \ No newline at end of file diff --git a/src/components/hooks/useOfferListDialog.js b/src/components/hooks/useOfferListDialog.js new file mode 100644 index 0000000..9bee676 --- /dev/null +++ b/src/components/hooks/useOfferListDialog.js @@ -0,0 +1,11 @@ +import useDialog from './useDialog'; + +export default function useOfferListDialog(refetch) { + const { dialogOpen, handleOpen, handleClose } = useDialog(refetch); + + return { + offerListingDialogOpen: dialogOpen, + handleOfferListDialogOpen: handleOpen, + handleOfferListDialogClose: handleClose, + }; +} diff --git a/src/graphql/query.js b/src/graphql/query.js index 28e8b95..2a1a1b3 100644 --- a/src/graphql/query.js +++ b/src/graphql/query.js @@ -97,6 +97,16 @@ export const GET_ALL_INTERVIEWS = gql` interviewer description status + jobApplication { + id + companyName + description + jobTitle + jobUrl + appliedDate + salaryRange + status + } } } `; diff --git a/src/utils/interviewUtil.js b/src/utils/interviewUtil.js new file mode 100644 index 0000000..e6fbca3 --- /dev/null +++ b/src/utils/interviewUtil.js @@ -0,0 +1,23 @@ +import dayjs from 'dayjs'; + +/** + * Filters interviews based on date and status. + * @param {Array} interviews - List of interview objects. + * @returns {Array} - Filtered interviews. + */ +export const getFilteredInterviews = (interviews) => { + return ( + interviews?.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) + ); + }) || [] + ); +};