719 lines
24 KiB
TypeScript
719 lines
24 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Paper,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
Typography,
|
|
FormControl,
|
|
Select,
|
|
MenuItem,
|
|
InputLabel,
|
|
Chip,
|
|
IconButton,
|
|
Dialog,
|
|
AppBar,
|
|
Toolbar,
|
|
useMediaQuery,
|
|
useTheme,
|
|
Slide,
|
|
Checkbox,
|
|
TablePagination,
|
|
TextField,
|
|
InputAdornment,
|
|
CircularProgress,
|
|
Alert,
|
|
Tooltip,
|
|
Grid,
|
|
SxProps,
|
|
} from '@mui/material';
|
|
import {
|
|
KeyboardArrowUp as ArrowUpIcon,
|
|
KeyboardArrowDown as ArrowDownIcon,
|
|
Business as BusinessIcon,
|
|
Work as WorkIcon,
|
|
Schedule as ScheduleIcon,
|
|
ArrowBack as ArrowBackIcon,
|
|
Search as SearchIcon,
|
|
Visibility as VisibilityIcon,
|
|
Edit as EditIcon,
|
|
Delete as DeleteIcon,
|
|
Close as CloseIcon,
|
|
} from '@mui/icons-material';
|
|
import { TransitionProps } from '@mui/material/transitions';
|
|
import * as Types from 'types/types'; // Adjust the import path as necessary
|
|
import { useAuth } from 'hooks/AuthContext';
|
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
|
import { Scrollable } from 'components/Scrollable';
|
|
import { useLocation } from 'react-router-dom';
|
|
|
|
// async searchJobs(query: string): Promise<Types.PaginatedResponse> {
|
|
// const results = await this.getJobs();
|
|
// const filtered = results.data.filter(job =>
|
|
// job.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
// job.description.toLowerCase().includes(query.toLowerCase()) ||
|
|
// job.company?.toLowerCase().includes(query.toLowerCase())
|
|
// );
|
|
// return {
|
|
// data: filtered,
|
|
// totalPages: 1,
|
|
// totalItems: filtered.length,
|
|
// };
|
|
// }
|
|
|
|
type SortField = 'updatedAt' | 'createdAt' | 'company' | 'title';
|
|
type SortOrder = 'asc' | 'desc';
|
|
|
|
interface JobsViewProps {
|
|
filter?: Record<string, string | number | boolean>;
|
|
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
|
onJobView?: (job: Types.Job) => void;
|
|
onJobEdit?: (job: Types.Job) => void;
|
|
onJobDelete?: (job: Types.Job) => Promise<void>;
|
|
selectable?: boolean;
|
|
showActions?: boolean;
|
|
showDetailsPanel?: boolean;
|
|
variant?: 'table' | 'list' | 'responsive';
|
|
sx?: SxProps;
|
|
}
|
|
|
|
const Transition = React.forwardRef(function Transition(
|
|
props: TransitionProps & {
|
|
children: React.ReactElement;
|
|
},
|
|
ref: React.Ref<unknown>
|
|
) {
|
|
return <Slide direction="up" ref={ref} {...props} />;
|
|
});
|
|
|
|
const JobInfoPanel: React.FC<{ job: Types.Job; onClose?: () => void; inDialog?: boolean }> = ({
|
|
job,
|
|
onClose,
|
|
inDialog = false,
|
|
}) => (
|
|
<Box
|
|
sx={{
|
|
p: inDialog ? 2 : 1.5,
|
|
height: '100%',
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
|
<Typography variant="h5" component="h1" gutterBottom>
|
|
{job.title}
|
|
</Typography>
|
|
{onClose && !inDialog && (
|
|
<IconButton onClick={onClose} size="small">
|
|
<CloseIcon />
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
|
|
<Typography variant="h6" color="primary" gutterBottom>
|
|
{job.company}
|
|
</Typography>
|
|
|
|
<Box sx={{ mb: 2 }}>
|
|
<Chip
|
|
label={job.details?.isActive ? 'Active' : 'Inactive'}
|
|
color={job.details?.isActive ? 'success' : 'default'}
|
|
size="small"
|
|
sx={{ mr: 1 }}
|
|
/>
|
|
{job.details?.employmentType && (
|
|
<Chip label={job.details.employmentType} variant="outlined" size="small" sx={{ mr: 1 }} />
|
|
)}
|
|
</Box>
|
|
|
|
{job.details?.location && (
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
📍 {job.details.location.city}, {job.details.location.state || job.details.location.country}
|
|
</Typography>
|
|
)}
|
|
|
|
<StyledMarkdown content={job.description} />
|
|
|
|
{job.requirements &&
|
|
job.requirements.technicalSkills &&
|
|
job.requirements.technicalSkills.required &&
|
|
job.requirements.technicalSkills.required.length > 0 && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<Typography variant="subtitle2" gutterBottom>
|
|
Required Skills
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
{job.requirements.technicalSkills.required.map(skill => (
|
|
<Chip key={skill} label={skill} size="small" variant="outlined" />
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Posted: {job.createdAt?.toLocaleDateString()}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Updated: {job.updatedAt?.toLocaleDateString()}
|
|
</Typography>
|
|
{/* {job.views && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Views: {job.views}
|
|
</Typography>
|
|
)} */}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
|
|
const JobsView: React.FC<JobsViewProps> = ({
|
|
onJobSelect,
|
|
onJobView,
|
|
onJobEdit,
|
|
onJobDelete,
|
|
selectable = true,
|
|
showActions = true,
|
|
showDetailsPanel = true,
|
|
filter = {},
|
|
sx = {},
|
|
}) => {
|
|
const theme = useTheme();
|
|
const { apiClient, user } = useAuth();
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
|
const location = useLocation();
|
|
|
|
const [jobs, setJobs] = React.useState<Types.Job[]>([]);
|
|
const [loading, setLoading] = React.useState<boolean>(true);
|
|
const [error, setError] = React.useState<string | null>(null);
|
|
const [page, setPage] = React.useState<number>(0);
|
|
const [limit, setLimit] = React.useState<number>(25);
|
|
const [total, setTotal] = React.useState<number>(0);
|
|
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
|
const [searchTimeout, setSearchTimeout] = React.useState<NodeJS.Timeout | null>(null);
|
|
const [selectedJobs, setSelectedJobs] = React.useState<Set<string>>(new Set());
|
|
const [selectedJob, setSelectedJob] = React.useState<Types.Job | null>(null);
|
|
const [sortField, setSortField] = React.useState<SortField>('updatedAt');
|
|
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
|
const [mobileDialogOpen, setMobileDialogOpen] = React.useState(false);
|
|
const [detailsPanelOpen, setDetailsPanelOpen] = React.useState(showDetailsPanel);
|
|
|
|
if (location.pathname.indexOf('/candidate/jobs') === 0) {
|
|
filter = { ...filter, owner_id: user?.id || '' };
|
|
}
|
|
|
|
const fetchJobs = React.useCallback(
|
|
async (pageNum = 0, searchTerm = '') => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
const paginationRequest: Partial<Types.PaginatedRequest> = {
|
|
page: pageNum + 1,
|
|
limit: limit,
|
|
sortBy: sortField,
|
|
sortOrder: sortOrder,
|
|
filters: filter,
|
|
};
|
|
|
|
let paginationResponse: Types.PaginatedResponse;
|
|
if (searchTerm.trim()) {
|
|
paginationResponse = await apiClient.searchJobs(searchTerm);
|
|
} else {
|
|
paginationResponse = await apiClient.getJobs(paginationRequest);
|
|
}
|
|
|
|
const sortedJobs = sortJobs(paginationResponse.data, sortField, sortOrder);
|
|
let updated = false;
|
|
if (jobs.length) {
|
|
if (sortedJobs.length !== jobs.length) {
|
|
updated = true;
|
|
} else {
|
|
for (let i = 0; i < sortedJobs.length; i++) {
|
|
if (sortedJobs[i].id !== jobs[i].id) {
|
|
updated = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
updated = true;
|
|
}
|
|
if (updated) {
|
|
setJobs(sortedJobs);
|
|
setTotal(paginationResponse.total);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred while fetching jobs');
|
|
setJobs([]);
|
|
setTotal(0);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[limit, sortField, sortOrder, apiClient]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (jobs.length > 0 && !selectedJob && detailsPanelOpen) {
|
|
console.log('Setting selected job from fetchJobs');
|
|
setSelectedJob(jobs[0]);
|
|
}
|
|
}, [jobs, selectedJob, detailsPanelOpen]);
|
|
|
|
React.useEffect(() => {
|
|
console.log('Fetching jobs with filter:', filter, 'searchQuery:', searchQuery);
|
|
fetchJobs(0, searchQuery);
|
|
}, [fetchJobs, searchQuery]);
|
|
|
|
const sortJobs = (jobsList: Types.Job[], field: SortField, order: SortOrder): Types.Job[] => {
|
|
return [...jobsList].sort((a, b) => {
|
|
let aValue: number | string;
|
|
let bValue: number | string;
|
|
|
|
switch (field) {
|
|
case 'updatedAt':
|
|
aValue = a.updatedAt?.getTime() || 0;
|
|
bValue = b.updatedAt?.getTime() || 0;
|
|
break;
|
|
case 'createdAt':
|
|
aValue = a.createdAt?.getTime() || 0;
|
|
bValue = b.createdAt?.getTime() || 0;
|
|
break;
|
|
case 'company':
|
|
aValue = a.company?.toLowerCase() || '';
|
|
bValue = b.company?.toLowerCase() || '';
|
|
break;
|
|
case 'title':
|
|
aValue = a.title?.toLowerCase() || '';
|
|
bValue = b.title?.toLowerCase() || '';
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
if (aValue < bValue) return order === 'asc' ? -1 : 1;
|
|
if (aValue > bValue) return order === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
};
|
|
|
|
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
console.log('Handling search change:', event.target.value);
|
|
const value = event.target.value;
|
|
setSearchQuery(value);
|
|
|
|
if (searchTimeout) {
|
|
clearTimeout(searchTimeout);
|
|
}
|
|
|
|
const timeout = setTimeout(() => {
|
|
setPage(0);
|
|
fetchJobs(0, value);
|
|
}, 500);
|
|
|
|
setSearchTimeout(timeout);
|
|
};
|
|
|
|
const handlePageChange = (event: unknown, newPage: number): void => {
|
|
console.log('Handling page change:', newPage);
|
|
setPage(newPage);
|
|
fetchJobs(newPage, searchQuery);
|
|
};
|
|
|
|
const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
console.log('Handling rows per page change:', event.target.value);
|
|
const newLimit = parseInt(event.target.value, 10);
|
|
setLimit(newLimit);
|
|
setPage(0);
|
|
fetchJobs(0, searchQuery);
|
|
};
|
|
|
|
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
if (event.target.checked) {
|
|
const newSelected = new Set<string>(jobs.map(job => job.id || ''));
|
|
setSelectedJobs(newSelected);
|
|
onJobSelect?.(jobs);
|
|
} else {
|
|
setSelectedJobs(new Set());
|
|
onJobSelect?.([]);
|
|
}
|
|
};
|
|
|
|
const handleSelectJob = (jobId: string): void => {
|
|
const newSelected = new Set(selectedJobs);
|
|
if (newSelected.has(jobId)) {
|
|
newSelected.delete(jobId);
|
|
} else {
|
|
newSelected.add(jobId);
|
|
}
|
|
setSelectedJobs(newSelected);
|
|
|
|
const selectedJobsList = jobs.filter(job => newSelected.has(job.id || ''));
|
|
onJobSelect?.(selectedJobsList);
|
|
};
|
|
|
|
const handleSort = (field: SortField): void => {
|
|
if (sortField === field) {
|
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortField(field);
|
|
setSortOrder('desc');
|
|
}
|
|
};
|
|
|
|
const handleJobRowClick = (job: Types.Job): void => {
|
|
setSelectedJob(job);
|
|
if (isMobile) {
|
|
setMobileDialogOpen(true);
|
|
} else if (detailsPanelOpen || !isMobile) {
|
|
setDetailsPanelOpen(true);
|
|
}
|
|
onJobView?.(job);
|
|
};
|
|
|
|
const getOwnerName = (owner?: Types.BaseUser): string => {
|
|
if (!owner) return 'Unknown';
|
|
return `${owner.firstName || ''} ${owner.lastName || ''}`.trim() || owner.email || 'Unknown';
|
|
};
|
|
|
|
const truncateDescription = (description: string, maxLength = 100): string => {
|
|
if (description.length <= maxLength) return description;
|
|
return description.substring(0, maxLength) + '...';
|
|
};
|
|
|
|
const formatDate = (date: Date | undefined): string => {
|
|
if (!date) return 'N/A';
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
...(isMobile ? {} : { year: 'numeric' }),
|
|
...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }),
|
|
}).format(date);
|
|
};
|
|
|
|
const getSortIcon = (field: SortField): React.ReactElement => {
|
|
if (sortField !== field) return <></>;
|
|
return sortOrder === 'asc' ? (
|
|
<ArrowUpIcon fontSize="small" />
|
|
) : (
|
|
<ArrowDownIcon fontSize="small" />
|
|
);
|
|
};
|
|
|
|
const isSelected = (jobId: string): boolean => selectedJobs.has(jobId);
|
|
const numSelected = selectedJobs.size;
|
|
const rowCount = jobs.length;
|
|
|
|
if (error) {
|
|
return (
|
|
<Paper sx={{ p: 2 }}>
|
|
<Alert severity="error">{error}</Alert>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const tableContent = (
|
|
<>
|
|
<Box sx={{ p: 2 }}>
|
|
<Grid container spacing={2} alignItems="center">
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="h6" component="h2">
|
|
Jobs ({total})
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
placeholder="Search jobs..."
|
|
value={searchQuery}
|
|
onChange={handleSearchChange}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
/>
|
|
<FormControl size="small" sx={{ minWidth: 150 }}>
|
|
<InputLabel>Sort</InputLabel>
|
|
<Select
|
|
value={`${sortField}-${sortOrder}`}
|
|
label="Sort"
|
|
onChange={(e): void => {
|
|
const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
|
|
setSortField(field);
|
|
setSortOrder(order);
|
|
}}
|
|
>
|
|
<MenuItem value="updatedAt-desc">Updated ↓</MenuItem>
|
|
<MenuItem value="updatedAt-asc">Updated ↑</MenuItem>
|
|
<MenuItem value="createdAt-desc">Created ↓</MenuItem>
|
|
<MenuItem value="createdAt-asc">Created ↑</MenuItem>
|
|
<MenuItem value="company-asc">Company A-Z</MenuItem>
|
|
<MenuItem value="company-desc">Company Z-A</MenuItem>
|
|
<MenuItem value="title-asc">Title A-Z</MenuItem>
|
|
<MenuItem value="title-desc">Title Z-A</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
|
|
<TableContainer>
|
|
<Table size="small" aria-label="jobs table">
|
|
<TableHead>
|
|
<TableRow sx={{ '& th': { whiteSpace: 'nowrap' } }}>
|
|
{selectable && (
|
|
<TableCell padding="checkbox">
|
|
<Checkbox
|
|
color="primary"
|
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
|
checked={rowCount > 0 && numSelected === rowCount}
|
|
onChange={handleSelectAll}
|
|
/>
|
|
</TableCell>
|
|
)}
|
|
<TableCell sx={{ cursor: 'pointer' }} onClick={(): void => handleSort('company')}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<BusinessIcon fontSize="small" />
|
|
Company {getSortIcon('company')}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell sx={{ cursor: 'pointer' }} onClick={(): void => handleSort('title')}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<WorkIcon fontSize="small" />
|
|
Title {getSortIcon('title')}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>Description</TableCell>
|
|
{!isMobile && (
|
|
<>
|
|
<TableCell>Owner</TableCell>
|
|
<TableCell
|
|
sx={{ cursor: 'pointer' }}
|
|
onClick={(): void => handleSort('updatedAt')}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<ScheduleIcon fontSize="small" />
|
|
Updated {getSortIcon('updatedAt')}
|
|
</Box>
|
|
</TableCell>
|
|
</>
|
|
)}
|
|
{/* <TableCell>Status</TableCell> */}
|
|
{!isMobile && showActions && <TableCell align="center">Actions</TableCell>}
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} align="center">
|
|
<CircularProgress size={24} />
|
|
</TableCell>
|
|
</TableRow>
|
|
) : jobs.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={8} align="center">
|
|
<Typography variant="body2" color="textSecondary">
|
|
No jobs found
|
|
</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
jobs.map(job => {
|
|
const isItemSelected = isSelected(job.id || '');
|
|
return (
|
|
<TableRow
|
|
key={job.id}
|
|
hover
|
|
selected={isItemSelected || selectedJob?.id === job.id}
|
|
onClick={(): void => handleJobRowClick(job)}
|
|
sx={{ cursor: 'pointer' }}
|
|
>
|
|
{selectable && (
|
|
<TableCell padding="checkbox" onClick={(e): void => e.stopPropagation()}>
|
|
<Checkbox
|
|
color="primary"
|
|
checked={isItemSelected}
|
|
onChange={(): void => handleSelectJob(job.id || '')}
|
|
/>
|
|
</TableCell>
|
|
)}
|
|
<TableCell>
|
|
<Typography variant="body2" fontWeight="medium">
|
|
{job.company}
|
|
</Typography>
|
|
{job.details?.location && (
|
|
<Typography variant="caption" color="text.secondary">
|
|
{job.details.location.city}, {job.details.location.state}
|
|
</Typography>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Typography variant="body2" fontWeight="medium">
|
|
{job.title}
|
|
</Typography>
|
|
{job.details?.employmentType && (
|
|
<Chip
|
|
label={job.details.employmentType}
|
|
size="small"
|
|
variant="outlined"
|
|
sx={{ mt: 0.5 }}
|
|
/>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Typography variant="body2" color="textSecondary">
|
|
{truncateDescription(job.summary || job.description || '', 100)}
|
|
</Typography>
|
|
</TableCell>
|
|
{!isMobile && (
|
|
<>
|
|
<TableCell>
|
|
<Typography variant="body2">{getOwnerName(job.owner)}</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Typography variant="body2">{formatDate(job.updatedAt)}</Typography>
|
|
</TableCell>
|
|
</>
|
|
)}
|
|
{/* <TableCell>
|
|
<Chip
|
|
label={job.details?.isActive ? 'Active' : 'Inactive'}
|
|
color={job.details?.isActive ? 'success' : 'default'}
|
|
size="small"
|
|
/>
|
|
</TableCell> */}
|
|
{!isMobile && showActions && (
|
|
<TableCell align="center" onClick={(e): void => e.stopPropagation()}>
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|
{onJobView && (
|
|
<Tooltip title="View Job">
|
|
<IconButton size="small" onClick={(): void => onJobView(job)}>
|
|
<VisibilityIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{onJobEdit && (
|
|
<Tooltip title="Edit Job">
|
|
<IconButton size="small" onClick={(): void => onJobEdit(job)}>
|
|
<EditIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
{onJobDelete && (
|
|
<Tooltip title="Delete Job">
|
|
<IconButton
|
|
size="small"
|
|
onClick={async (): Promise<void> => {
|
|
await onJobDelete(job);
|
|
fetchJobs(0, searchQuery);
|
|
}}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
);
|
|
})
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
|
|
<TablePagination
|
|
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
component="div"
|
|
count={total}
|
|
rowsPerPage={limit}
|
|
page={page}
|
|
onPageChange={handlePageChange}
|
|
onRowsPerPageChange={handleRowsPerPageChange}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', position: 'relative', ...sx }}>
|
|
<Scrollable
|
|
sx={{ display: 'flex', flex: 1, flexDirection: 'column', height: '100%', width: '100%' }}
|
|
>
|
|
<Paper sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>{tableContent}</Paper>
|
|
</Scrollable>
|
|
|
|
{detailsPanelOpen && !isMobile && (
|
|
<Scrollable
|
|
sx={{ display: 'flex', flex: 1, flexDirection: 'row', height: '100%', width: '100%' }}
|
|
>
|
|
<Paper sx={{ flex: 1, ml: 1 }}>
|
|
{selectedJob ? (
|
|
<JobInfoPanel
|
|
job={selectedJob}
|
|
onClose={(): void => {
|
|
console.log('Closing JobInfoPanel');
|
|
setDetailsPanelOpen(false);
|
|
setSelectedJob(null);
|
|
}}
|
|
/>
|
|
) : (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100%',
|
|
color: 'text.secondary',
|
|
textAlign: 'center',
|
|
p: 2,
|
|
}}
|
|
>
|
|
<Typography variant="body2">Select a job to view details</Typography>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</Scrollable>
|
|
)}
|
|
|
|
<Dialog
|
|
fullScreen
|
|
open={mobileDialogOpen}
|
|
onClose={(): void => setMobileDialogOpen(false)}
|
|
TransitionComponent={Transition}
|
|
>
|
|
<AppBar sx={{ position: 'relative' }}>
|
|
<Toolbar>
|
|
<IconButton
|
|
edge="start"
|
|
color="inherit"
|
|
onClick={(): void => setMobileDialogOpen(false)}
|
|
>
|
|
<ArrowBackIcon />
|
|
</IconButton>
|
|
<Box sx={{ ml: 2, flex: 1 }}>
|
|
<Typography variant="h6" noWrap>
|
|
{selectedJob?.title}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ opacity: 0.8 }} noWrap>
|
|
{selectedJob?.company}
|
|
</Typography>
|
|
</Box>
|
|
</Toolbar>
|
|
</AppBar>
|
|
{selectedJob && <JobInfoPanel job={selectedJob} inDialog />}
|
|
</Dialog>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export { JobsView };
|