Skip to content

Commit

Permalink
interview list (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
januschung authored Dec 21, 2024
1 parent 9300b12 commit 0c60eb3
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 98 deletions.
103 changes: 57 additions & 46 deletions src/components/AppHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;

Expand Down Expand Up @@ -96,6 +93,11 @@ export default function AppHeader() {
handleClose={handleProfileClose}
setOpen={setProfileOpen}
/>
<InterviewListDialog
open={interviewListingDialogOpen}
handleClose={handleInterviewListDialogClose}
setOpen={handleInterviewListDialogOpen}
/>
<OfferListDialog
open={offerListingDialogOpen}
handleClose={handleOfferListDialogClose}
Expand Down Expand Up @@ -124,30 +126,38 @@ export default function AppHeader() {
<SearchBar onSearch={handleSearch} />
<Box sx={{ flexGrow: 1 }} />
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
<IconButton size="large" aria-label="New" color="inherit" onClick={handleOpen}>
<AddCircleIcon />
</IconButton>
<IconButton size="large" color="inherit" onClick={() => refetch()}>
<Badge badgeContent={interviewsLoading ? '...' : interviewCount} color="error">
<EventAvailableIcon />
</Badge>
</IconButton>
<IconButton size="large" color="inherit" onClick={handleOfferListDialogOpen}>
<Badge badgeContent={offersLoading ? '...' : offerCount} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
<IconButton
size="large"
edge="end"
aria-label="account of current user"
aria-controls="primary-search-account-menu"
aria-haspopup="true"
onClick={handleProfileMenuOpen}
color="inherit"
>
<AccountCircle />
</IconButton>
<Tooltip title="New Job Application">
<IconButton size="large" aria-label="New" color="inherit" onClick={handleOpen}>
<AddCircleIcon />
</IconButton>
</Tooltip>
<Tooltip title="Interview List">
<IconButton size="large" color="inherit" onClick={handleInterviewListDialogOpen}>
<Badge badgeContent={interviewsLoading ? '...' : interviewCount} color="error">
<EventAvailableIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Offer List">
<IconButton size="large" color="inherit" onClick={handleOfferListDialogOpen}>
<Badge badgeContent={offersLoading ? '...' : offerCount} color="error">
<NotificationsIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Profile">
<IconButton
size="large"
edge="end"
aria-label="account of current user"
aria-controls="primary-search-account-menu"
aria-haspopup="true"
onClick={handleProfileMenuOpen}
color="inherit"
>
<AccountCircle />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
<IconButton
Expand All @@ -168,6 +178,7 @@ export default function AppHeader() {
handleMobileMenuClose={handleMobileMenuClose}
handleProfileMenuOpen={handleProfileMenuOpen}
handleJobApplicationOpen={handleOpen}
handleInterviewListDialogOpen={handleInterviewListDialogOpen}
handleOfferListDialogOpen={handleOfferListDialogOpen}
interviewCount={interviewCount}
offerCount={offerCount}
Expand Down
121 changes: 121 additions & 0 deletions src/components/InterviewListDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import CancelIcon from '@mui/icons-material/Cancel';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { GET_ALL_INTERVIEWS } from '../graphql/query';
import useJobApplicationDialog from './hooks/useJobApplicationDialog';
import JobApplicationDialog from './JobApplicationDialog';
import DialogTitleBar from './DialogTitleBar';
import useSortableTable from './hooks/useSortableTable';
import SortableTable from './common/SortableTable';
import { getFilteredInterviews } from '../utils/interviewUtil';

export default function InterviewListDialog({ handleClose, open }) {
const [interviews, setInterviews] = useState([]);

const { loading, error, data, refetch } = useQuery(GET_ALL_INTERVIEWS, {
fetchPolicy: 'network-only',
onCompleted: (data) => 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) => (
<Button
size="small"
color="info"
variant="outlined"
onClick={() => handleJobDialogOpen(row.jobApplication)}
>
Job Details
</Button>
),
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 (
<Dialog
open={open}
onClose={handleClose}
fullWidth
maxWidth="md"
slotProps={{
backdrop: {
sx: {
backdropFilter: 'blur(8px)',
},
},
}}
>
<DialogTitleBar title="Interview List" />
<DialogContent dividers>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" height="200px">
<CircularProgress />
</Box>
) : error ? (
<Typography variant="body1" color="error" align="center">
Something went wrong. Please try again later.
</Typography>
) : (
<SortableTable
data={sortedData}
columns={columns}
handleSort={handleSort}
getSortIndicator={getSortIndicator}
/>
)}
</DialogContent>
<DialogActions>
<Button color="info" variant="outlined" startIcon={<CancelIcon />} onClick={handleClose}>
Cancel
</Button>
</DialogActions>
<JobApplicationDialog
jobApplication={jobApplication}
open={jobDialogOpen}
handleClose={handleJobDialogClose}
/>
</Dialog>
);
}
4 changes: 3 additions & 1 deletion src/components/JobApplicationDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function JobApplicationDialog({ jobApplication, handleClose, open
}}
>
<DialogTitleBar title={(isNew ? 'Add ' : 'Edit ') + 'Job Application'} />
{!isNew && (
<Tabs
value={activeTab}
onChange={handleTabChange}
Expand All @@ -46,6 +47,7 @@ export default function JobApplicationDialog({ jobApplication, handleClose, open
<Tab label="Interviews" />
<Tab label="Offer" />
</Tabs>
)}
{activeTab === 0 && (
<ApplicationForm
jobApplication={jobApplication}
Expand All @@ -56,7 +58,7 @@ export default function JobApplicationDialog({ jobApplication, handleClose, open
)}
{activeTab === 1 && (
<InterviewsForm
jobApplicationId={parseInt(jobApplication.id || 0)}
jobApplicationId={parseInt(jobApplication?.id || 0)}
/>
)}

Expand Down
19 changes: 5 additions & 14 deletions src/components/JobApplicationList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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({
Expand All @@ -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) =>
Expand Down Expand Up @@ -176,8 +167,8 @@ export default function JobApplicationList({ searchTerm }) {
)}
</Container>
<ConfirmDialog
open={confirmOpen}
onCancel={cancel}
open={confirmDialogOpen}
onCancel={handleConfirmDialogClose}
onConfirm={confirmDeleteJobApplication}
title="Confirm Deletion"
content="Are you sure you want to delete this job application? This action cannot be undone."
Expand Down
4 changes: 2 additions & 2 deletions src/components/MobileMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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, handleOfferListDialogOpen, interviewCount, offerCount }) {
export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handleMobileMenuClose, handleProfileMenuOpen, handleJobApplicationOpen, handleInterviewListDialogOpen, handleOfferListDialogOpen, interviewCount, offerCount }) {
const mobileMenuId = 'primary-search-account-menu-mobile';
return (
<Menu
Expand All @@ -32,7 +32,7 @@ export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handl
</IconButton>
<p>New</p>
</MenuItem>
<MenuItem>
<MenuItem onClick={handleInterviewListDialogOpen}>
<IconButton size="large" color="inherit">
<Badge badgeContent={interviewCount} color="error">
<EventAvailableIcon />
Expand Down
Loading

0 comments on commit 0c60eb3

Please sign in to comment.