Compare commits
2 Commits
5e37e17724
...
e0992e77b2
Author | SHA1 | Date | |
---|---|---|---|
e0992e77b2 | |||
71d27f5ace |
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect, JSX } from 'react';
|
||||||
import { Box, Link, Typography, Avatar, SxProps, Tooltip, IconButton } from '@mui/material';
|
import { Box, Link, Typography, Avatar, SxProps, Tooltip, IconButton } from '@mui/material';
|
||||||
import { Divider, useTheme } from '@mui/material';
|
import { Divider, useTheme } from '@mui/material';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
@ -17,6 +17,73 @@ interface CandidateInfoProps {
|
|||||||
variant?: 'minimal' | 'small' | 'normal' | undefined;
|
variant?: 'minimal' | 'small' | 'normal' | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExpandingTextProps {
|
||||||
|
content: string;
|
||||||
|
lines?: number;
|
||||||
|
sx?: SxProps;
|
||||||
|
}
|
||||||
|
const ExpandingText = (props: ExpandingTextProps): JSX.Element => {
|
||||||
|
const { content, sx, lines = 3 } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [isTextExpanded, setIsTextExpanded] = useState<boolean>(false);
|
||||||
|
const [shouldShowMoreButton, setShouldShowMoreButton] = useState<boolean>(false);
|
||||||
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current && content) {
|
||||||
|
const element = textRef.current;
|
||||||
|
// Check if the scrollHeight is greater than clientHeight (meaning content is truncated)
|
||||||
|
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: `${lines}rem`, ...sx }}>
|
||||||
|
<Typography
|
||||||
|
ref={textRef}
|
||||||
|
variant="body1"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: isTextExpanded ? 'unset' : lines,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
fontSize: '0.8rem !important',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Typography>
|
||||||
|
{shouldShowMoreButton && (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={(e): void => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsTextExpanded(!isTextExpanded);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
textDecoration: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.725rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
mt: 0.5,
|
||||||
|
display: 'block',
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
[{isTextExpanded ? 'less' : 'more'}]
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
|
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
|
||||||
const { candidate } = props;
|
const { candidate } = props;
|
||||||
const { user, apiClient } = useAuth();
|
const { user, apiClient } = useAuth();
|
||||||
@ -26,20 +93,6 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
|||||||
const ai: CandidateAI | null = 'isAI' in candidate ? (candidate as CandidateAI) : null;
|
const ai: CandidateAI | null = 'isAI' in candidate ? (candidate as CandidateAI) : null;
|
||||||
const isAdmin = user?.isAdmin;
|
const isAdmin = user?.isAdmin;
|
||||||
|
|
||||||
// State for description expansion
|
|
||||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
|
||||||
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
|
||||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Check if description needs truncation
|
|
||||||
useEffect(() => {
|
|
||||||
if (descriptionRef.current && candidate.description) {
|
|
||||||
const element = descriptionRef.current;
|
|
||||||
// Check if the scrollHeight is greater than clientHeight (meaning content is truncated)
|
|
||||||
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
|
|
||||||
}
|
|
||||||
}, [candidate.description]);
|
|
||||||
|
|
||||||
const deleteCandidate = async (candidateId: string | undefined): Promise<void> => {
|
const deleteCandidate = async (candidateId: string | undefined): Promise<void> => {
|
||||||
if (candidateId) {
|
if (candidateId) {
|
||||||
await apiClient.deleteCandidate(candidateId);
|
await apiClient.deleteCandidate(candidateId);
|
||||||
@ -68,7 +121,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
|||||||
>
|
>
|
||||||
{ai && <AIBanner variant={variant} />}
|
{ai && <AIBanner variant={variant} />}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'row', flexGrow: 1 }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
|
||||||
alt={`${candidate.fullName}'s profile`}
|
alt={`${candidate.fullName}'s profile`}
|
||||||
@ -128,53 +181,13 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{variant === 'normal' && (
|
||||||
|
<ExpandingText
|
||||||
|
sx={{ display: 'flex', flexShrink: 1 }}
|
||||||
|
content={candidate.description || ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
{!isMobile && variant === 'normal' && (
|
|
||||||
<Box sx={{ minHeight: '5rem' }}>
|
|
||||||
<Typography
|
|
||||||
ref={descriptionRef}
|
|
||||||
variant="body1"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
fontSize: '0.8rem !important',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{candidate.description}
|
|
||||||
</Typography>
|
|
||||||
{shouldShowMoreButton && (
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
onClick={(e): void => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
textDecoration: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '0.725rem',
|
|
||||||
fontWeight: 500,
|
|
||||||
mt: 0.5,
|
|
||||||
display: 'block',
|
|
||||||
'&:hover': {
|
|
||||||
textDecoration: 'underline',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
[{isDescriptionExpanded ? 'less' : 'more'}]
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{variant !== 'small' && variant !== 'minimal' && (
|
{variant !== 'small' && variant !== 'minimal' && (
|
||||||
<>
|
<>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
|
@ -1,364 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Checkbox,
|
|
||||||
TablePagination,
|
|
||||||
TextField,
|
|
||||||
InputAdornment,
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Chip,
|
|
||||||
CircularProgress,
|
|
||||||
Alert,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {
|
|
||||||
Search as SearchIcon,
|
|
||||||
Visibility as VisibilityIcon,
|
|
||||||
Edit as EditIcon,
|
|
||||||
Delete as DeleteIcon,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
|
|
||||||
import * as Types from 'types/types';
|
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
|
||||||
|
|
||||||
interface JobsTableProps {
|
|
||||||
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
|
||||||
onJobView?: (job: Types.Job) => void;
|
|
||||||
onJobEdit?: (job: Types.Job) => void;
|
|
||||||
onJobDelete?: (job: Types.Job) => void;
|
|
||||||
selectable?: boolean;
|
|
||||||
showActions?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const JobsTable: React.FC<JobsTableProps> = ({
|
|
||||||
onJobSelect,
|
|
||||||
onJobView,
|
|
||||||
onJobEdit,
|
|
||||||
onJobDelete,
|
|
||||||
selectable = true,
|
|
||||||
showActions = true,
|
|
||||||
}) => {
|
|
||||||
const { apiClient } = useAuth();
|
|
||||||
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());
|
|
||||||
|
|
||||||
// Fetch jobs from API
|
|
||||||
const fetchJobs = React.useCallback(
|
|
||||||
async (pageNum = 0, searchTerm = '') => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
const paginationRequest: Partial<Types.PaginatedRequest> = {
|
|
||||||
page: pageNum + 1,
|
|
||||||
limit: limit,
|
|
||||||
sortBy: 'createdAt',
|
|
||||||
sortOrder: 'desc',
|
|
||||||
};
|
|
||||||
|
|
||||||
let paginationResponse: Types.PaginatedResponse;
|
|
||||||
if (searchTerm.trim()) {
|
|
||||||
paginationResponse = await apiClient.searchJobs(searchTerm);
|
|
||||||
} else {
|
|
||||||
paginationResponse = await apiClient.getJobs(paginationRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
setJobs(paginationResponse.data);
|
|
||||||
setTotal(paginationResponse.totalPages);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred while fetching jobs');
|
|
||||||
setJobs([]);
|
|
||||||
setTotal(0);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[limit, apiClient]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initial load
|
|
||||||
React.useEffect(() => {
|
|
||||||
fetchJobs(0, searchQuery);
|
|
||||||
}, [fetchJobs, searchQuery]);
|
|
||||||
|
|
||||||
// Handle search with debouncing
|
|
||||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
const value = event.target.value;
|
|
||||||
setSearchQuery(value);
|
|
||||||
|
|
||||||
if (searchTimeout) {
|
|
||||||
clearTimeout(searchTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setPage(0);
|
|
||||||
fetchJobs(0, value);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setSearchTimeout(timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle page change
|
|
||||||
const handlePageChange = (event: unknown, newPage: number): void => {
|
|
||||||
setPage(newPage);
|
|
||||||
fetchJobs(newPage, searchQuery);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle rows per page change
|
|
||||||
const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
const newLimit = parseInt(event.target.value, 10);
|
|
||||||
setLimit(newLimit);
|
|
||||||
setPage(0);
|
|
||||||
fetchJobs(0, searchQuery);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle selection
|
|
||||||
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 getOwnerName = (owner?: Types.Job['owner']): 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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper>
|
|
||||||
<Box sx={{ p: 2 }}>
|
|
||||||
<Typography variant="h6" component="h2" sx={{ mb: 2 }}>
|
|
||||||
Jobs ({total})
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
placeholder="Search jobs by title, description, or skills..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<SearchIcon />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
sx={{ mb: 2 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TableContainer>
|
|
||||||
<Table size="small" aria-label="jobs table">
|
|
||||||
<TableHead sx={{ '& th': { whiteSpace: 'nowrap' } }}>
|
|
||||||
<TableRow>
|
|
||||||
{selectable && (
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
color="primary"
|
|
||||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
|
||||||
checked={rowCount > 0 && numSelected === rowCount}
|
|
||||||
onChange={handleSelectAll}
|
|
||||||
inputProps={{
|
|
||||||
'aria-label': 'select all jobs',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
<TableCell>Title</TableCell>
|
|
||||||
<TableCell>Description</TableCell>
|
|
||||||
{/* <TableCell>Skills</TableCell> */}
|
|
||||||
<TableCell>Owner</TableCell>
|
|
||||||
{/* <TableCell align="right">Views</TableCell> */}
|
|
||||||
<TableCell>Status</TableCell>
|
|
||||||
<TableCell>Created</TableCell>
|
|
||||||
{showActions && <TableCell align="center">Actions</TableCell>}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody sx={{ '& p, & td': { m: 0, p: 0.5, fontSize: '0.75rem !important' } }}>
|
|
||||||
{loading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={selectable ? (showActions ? 9 : 8) : showActions ? 8 : 7}
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<CircularProgress size={24} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : jobs.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={selectable ? (showActions ? 9 : 8) : showActions ? 8 : 7}
|
|
||||||
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
|
|
||||||
role="checkbox"
|
|
||||||
aria-checked={isItemSelected}
|
|
||||||
selected={isItemSelected}
|
|
||||||
onClick={(): void => {
|
|
||||||
!selectable && handleSelectJob(job.id || '');
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
'&:last-child td, &:last-child th': { border: 0 },
|
|
||||||
cursor: selectable ? 'default' : 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectable && (
|
|
||||||
<TableCell padding="checkbox">
|
|
||||||
<Checkbox
|
|
||||||
color="primary"
|
|
||||||
checked={isItemSelected}
|
|
||||||
onChange={(): void => handleSelectJob(job.id || '')}
|
|
||||||
inputProps={{
|
|
||||||
'aria-labelledby': `job-${job.id}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
<TableCell scope="row" id={`job-${job.id}`}>
|
|
||||||
<Typography variant="body2" fontWeight="medium">
|
|
||||||
{job.title}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
{truncateDescription(job.description)}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
{/* <TableCell>
|
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
||||||
{job.skills?.slice(0, 3).map((skill) => (
|
|
||||||
<Chip key={skill} label={skill} size="small" variant="outlined" />
|
|
||||||
))}
|
|
||||||
{job.skills && job.skills.length > 3 && (
|
|
||||||
<Chip label={`+${job.skills.length - 3}`} size="small" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</TableCell> */}
|
|
||||||
<TableCell>
|
|
||||||
<Typography variant="body2">{getOwnerName(job.owner)}</Typography>
|
|
||||||
</TableCell>
|
|
||||||
{/* <TableCell align="right">
|
|
||||||
<Typography variant="body2">
|
|
||||||
{job.views}
|
|
||||||
</Typography>
|
|
||||||
</TableCell> */}
|
|
||||||
<TableCell>
|
|
||||||
<Chip
|
|
||||||
label={job.details?.isActive ? 'Active' : 'Inactive'}
|
|
||||||
color={job.details?.isActive ? 'success' : 'default'}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ whiteSpace: 'nowrap' }}>
|
|
||||||
<Typography variant="body2">{job.createdAt?.toLocaleDateString()}</Typography>
|
|
||||||
</TableCell>
|
|
||||||
{showActions && (
|
|
||||||
<TableCell align="center">
|
|
||||||
<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={(): void => onJobDelete(job)}>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { JobsTable };
|
|
@ -48,6 +48,7 @@ import * as Types from 'types/types'; // Adjust the import path as necessary
|
|||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||||
import { Scrollable } from 'components/Scrollable';
|
import { Scrollable } from 'components/Scrollable';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
// async searchJobs(query: string): Promise<Types.PaginatedResponse> {
|
// async searchJobs(query: string): Promise<Types.PaginatedResponse> {
|
||||||
// const results = await this.getJobs();
|
// const results = await this.getJobs();
|
||||||
@ -67,6 +68,7 @@ type SortField = 'updatedAt' | 'createdAt' | 'company' | 'title';
|
|||||||
type SortOrder = 'asc' | 'desc';
|
type SortOrder = 'asc' | 'desc';
|
||||||
|
|
||||||
interface JobsViewProps {
|
interface JobsViewProps {
|
||||||
|
filter?: Record<string, string | number | boolean>;
|
||||||
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
||||||
onJobView?: (job: Types.Job) => void;
|
onJobView?: (job: Types.Job) => void;
|
||||||
onJobEdit?: (job: Types.Job) => void;
|
onJobEdit?: (job: Types.Job) => void;
|
||||||
@ -172,11 +174,13 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
selectable = true,
|
selectable = true,
|
||||||
showActions = true,
|
showActions = true,
|
||||||
showDetailsPanel = true,
|
showDetailsPanel = true,
|
||||||
|
filter = {},
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { apiClient } = useAuth();
|
const { apiClient, user } = useAuth();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [jobs, setJobs] = React.useState<Types.Job[]>([]);
|
const [jobs, setJobs] = React.useState<Types.Job[]>([]);
|
||||||
const [loading, setLoading] = React.useState<boolean>(true);
|
const [loading, setLoading] = React.useState<boolean>(true);
|
||||||
@ -192,7 +196,9 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
||||||
const [mobileDialogOpen, setMobileDialogOpen] = React.useState(false);
|
const [mobileDialogOpen, setMobileDialogOpen] = React.useState(false);
|
||||||
const [detailsPanelOpen, setDetailsPanelOpen] = React.useState(showDetailsPanel);
|
const [detailsPanelOpen, setDetailsPanelOpen] = React.useState(showDetailsPanel);
|
||||||
|
if (location.pathname.indexOf('/candidate/jobs') === 0) {
|
||||||
|
filter = { ...filter, owner_id: user?.id || '' };
|
||||||
|
}
|
||||||
const fetchJobs = React.useCallback(
|
const fetchJobs = React.useCallback(
|
||||||
async (pageNum = 0, searchTerm = '') => {
|
async (pageNum = 0, searchTerm = '') => {
|
||||||
try {
|
try {
|
||||||
@ -203,6 +209,7 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
limit: limit,
|
limit: limit,
|
||||||
sortBy: sortField,
|
sortBy: sortField,
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
|
filters: filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
let paginationResponse: Types.PaginatedResponse;
|
let paginationResponse: Types.PaginatedResponse;
|
||||||
@ -439,7 +446,7 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table size="small" aria-label="jobs table">
|
<Table size="small" aria-label="jobs table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow sx={{ '& th': { whiteSpace: 'nowrap' } }}>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<TableCell padding="checkbox">
|
<TableCell padding="checkbox">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -534,7 +541,7 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Typography variant="body2" color="textSecondary">
|
||||||
{truncateDescription(job.description)}
|
{truncateDescription(job.summary || job.description || '', 100)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
@ -30,7 +30,6 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { JobsView } from 'components/ui/JobsView';
|
import { JobsView } from 'components/ui/JobsView';
|
||||||
import { ResumeViewer } from 'components/ui/ResumeViewer';
|
import { ResumeViewer } from 'components/ui/ResumeViewer';
|
||||||
|
|
||||||
import { JobsTable } from 'components/ui/JobsTable';
|
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
|
|
||||||
const LogoutPage = (): JSX.Element => {
|
const LogoutPage = (): JSX.Element => {
|
||||||
@ -128,20 +127,8 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
path: '/candidate/jobs/:jobId?',
|
path: '/candidate/jobs/:jobId?',
|
||||||
icon: <WorkIcon />,
|
icon: <WorkIcon />,
|
||||||
component: <JobsView />,
|
|
||||||
variant: 'fullWidth',
|
|
||||||
userTypes: ['candidate', 'guest', 'employer'],
|
|
||||||
showInNavigation: false,
|
|
||||||
showInUserMenu: true,
|
|
||||||
userMenuGroup: 'profile',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'jobs-table',
|
|
||||||
label: 'Jobs Table',
|
|
||||||
path: '/candidate/jobs-table/:jobId?',
|
|
||||||
icon: <WorkIcon />,
|
|
||||||
component: (
|
component: (
|
||||||
<JobsTable
|
<JobsView
|
||||||
onJobSelect={(selectedJobs: Types.Job[]): void => console.log('Selected:', selectedJobs)}
|
onJobSelect={(selectedJobs: Types.Job[]): void => console.log('Selected:', selectedJobs)}
|
||||||
onJobView={(job: Types.Job): void => console.log('View job:', job)}
|
onJobView={(job: Types.Job): void => console.log('View job:', job)}
|
||||||
onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)}
|
onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)}
|
||||||
@ -150,6 +137,7 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
showActions={true}
|
showActions={true}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
variant: 'fullWidth',
|
||||||
userTypes: ['candidate', 'guest', 'employer'],
|
userTypes: ['candidate', 'guest', 'employer'],
|
||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
showInUserMenu: true,
|
showInUserMenu: true,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ChatMessageStreaming,
|
ChatMessageStreaming,
|
||||||
ChatMessageStatus,
|
ChatMessageStatus,
|
||||||
ChatMessageMetaData,
|
ChatMessageMetaData,
|
||||||
|
CandidateQuestion,
|
||||||
} from 'types/types';
|
} from 'types/types';
|
||||||
import { ConversationHandle } from 'components/Conversation';
|
import { ConversationHandle } from 'components/Conversation';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
@ -50,7 +51,7 @@ const defaultMessage: ChatMessage = {
|
|||||||
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
||||||
(_props: BackstoryPageProps, ref): JSX.Element => {
|
(_props: BackstoryPageProps, ref): JSX.Element => {
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate } = useSelectedCandidate();
|
||||||
const [processingMessage, setProcessingMessage] = useState<
|
const [processingMessage, setProcessingMessage] = useState<
|
||||||
ChatMessageStatus | ChatMessageError | null
|
ChatMessageStatus | ChatMessageError | null
|
||||||
>(null);
|
>(null);
|
||||||
@ -219,6 +220,10 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
metadata: emptyMetadata,
|
metadata: emptyMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmitQuestion = (question: CandidateQuestion): void => {
|
||||||
|
sendMessage(question.question);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -241,7 +246,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
action={`Chat with Backstory about ${selectedCandidate.firstName}`}
|
action={`Chat with Backstory about ${selectedCandidate.firstName}`}
|
||||||
elevation={4}
|
elevation={4}
|
||||||
candidate={selectedCandidate}
|
candidate={selectedCandidate}
|
||||||
variant="small"
|
variant="normal"
|
||||||
sx={{
|
sx={{
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -249,7 +254,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
minHeight: 'min-content',
|
minHeight: 'min-content',
|
||||||
}} // Prevent header from shrinking
|
}} // Prevent header from shrinking
|
||||||
/>
|
/>
|
||||||
<Button
|
{/* <Button
|
||||||
sx={{ maxWidth: 'max-content' }}
|
sx={{ maxWidth: 'max-content' }}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setSelectedCandidate(null);
|
setSelectedCandidate(null);
|
||||||
@ -257,7 +262,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
Change Candidates
|
Change Candidates
|
||||||
</Button>
|
</Button> */}
|
||||||
</Paper>
|
</Paper>
|
||||||
{/* Chat Interface */}
|
{/* Chat Interface */}
|
||||||
{/* Scrollable Messages Area */}
|
{/* Scrollable Messages Area */}
|
||||||
@ -308,8 +313,14 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
|
|||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
)}
|
)}
|
||||||
{selectedCandidate.questions?.length !== 0 &&
|
{selectedCandidate.questions?.length !== 0 && (
|
||||||
selectedCandidate.questions?.map((q, i) => <BackstoryQuery key={i} question={q} />)}
|
<Box sx={{ diplay: 'flex', flexDirection: 'column', gap: 1, p: 1 }}>
|
||||||
|
{' '}
|
||||||
|
{selectedCandidate.questions?.map((q, i) => (
|
||||||
|
<BackstoryQuery key={i} question={q} submitQuery={handleSubmitQuestion} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{/* Fixed Message Input */}
|
{/* Fixed Message Input */}
|
||||||
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
|
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
|
||||||
<DeleteConfirmation
|
<DeleteConfirmation
|
||||||
|
@ -104,6 +104,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const { user, updateUserData, apiClient } = useAuth();
|
const { user, updateUserData, apiClient } = useAuth();
|
||||||
const chatRef = React.useRef<ConversationHandle>(null);
|
const chatRef = React.useRef<ConversationHandle>(null);
|
||||||
|
const [dirty, setDirty] = useState(false);
|
||||||
|
|
||||||
// Check if user is a candidate
|
// Check if user is a candidate
|
||||||
const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null;
|
const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null;
|
||||||
@ -119,7 +120,6 @@ const CandidateProfile: React.FC = () => {
|
|||||||
|
|
||||||
// Dialog states
|
// Dialog states
|
||||||
const [skillDialog, setSkillDialog] = useState(false);
|
const [skillDialog, setSkillDialog] = useState(false);
|
||||||
const [questionDialog, setQuestionDialog] = useState(false);
|
|
||||||
const [experienceDialog, setExperienceDialog] = useState(false);
|
const [experienceDialog, setExperienceDialog] = useState(false);
|
||||||
|
|
||||||
// New item states
|
// New item states
|
||||||
@ -144,6 +144,13 @@ const CandidateProfile: React.FC = () => {
|
|||||||
location: { city: '', country: '' },
|
location: { city: '', country: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dirty) {
|
||||||
|
handleSave();
|
||||||
|
setDirty(false);
|
||||||
|
}
|
||||||
|
}, [formData, dirty, setDirty]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
setFormData(candidate);
|
setFormData(candidate);
|
||||||
@ -208,14 +215,13 @@ const CandidateProfile: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Save changes
|
// Save changes
|
||||||
const handleSave = async (section: string): Promise<void> => {
|
const handleSave = async (): Promise<void> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (candidate.id) {
|
if (candidate.id) {
|
||||||
const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData);
|
const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData);
|
||||||
updateUserData(updatedCandidate);
|
updateUserData(updatedCandidate);
|
||||||
setSnack('Profile updated successfully!');
|
setSnack('Profile updated successfully!');
|
||||||
toggleEditMode(section);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSnack('Failed to update profile. Please try again.', 'error');
|
setSnack('Failed to update profile. Please try again.', 'error');
|
||||||
@ -243,6 +249,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
});
|
});
|
||||||
setSkillDialog(false);
|
setSkillDialog(false);
|
||||||
setSnack('Skill added successfully!');
|
setSnack('Skill added successfully!');
|
||||||
|
setDirty(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -251,6 +258,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
|
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, skills: updatedSkills });
|
setFormData({ ...formData, skills: updatedSkills });
|
||||||
setSnack('Skill removed successfully!');
|
setSnack('Skill removed successfully!');
|
||||||
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add new question
|
// Add new question
|
||||||
@ -264,8 +272,9 @@ const CandidateProfile: React.FC = () => {
|
|||||||
setNewQuestion({
|
setNewQuestion({
|
||||||
question: '',
|
question: '',
|
||||||
});
|
});
|
||||||
setQuestionDialog(false);
|
setEditMode({ ...editMode, questions: false });
|
||||||
setSnack('Question added successfully!');
|
setSnack('Question added successfully!');
|
||||||
|
setDirty(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -274,6 +283,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
const updatedQuestions = (formData.questions || []).filter((_, i) => i !== index);
|
const updatedQuestions = (formData.questions || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, questions: updatedQuestions });
|
setFormData({ ...formData, questions: updatedQuestions });
|
||||||
setSnack('Question removed successfully!');
|
setSnack('Question removed successfully!');
|
||||||
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add new work experience
|
// Add new work experience
|
||||||
@ -303,6 +313,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
|
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, experience: updatedExperience });
|
setFormData({ ...formData, experience: updatedExperience });
|
||||||
setSnack('Experience removed successfully!');
|
setSnack('Experience removed successfully!');
|
||||||
|
setDirty(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic Information Tab
|
// Basic Information Tab
|
||||||
@ -515,7 +526,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
handleSave('basic');
|
handleSave();
|
||||||
}}
|
}}
|
||||||
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -668,7 +679,9 @@ const CandidateProfile: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
onClick={(): void => setQuestionDialog(true)}
|
onClick={(): void => {
|
||||||
|
setEditMode({ ...editMode, questions: true });
|
||||||
|
}}
|
||||||
fullWidth={isMobile}
|
fullWidth={isMobile}
|
||||||
size={isMobile ? 'small' : 'medium'}
|
size={isMobile ? 'small' : 'medium'}
|
||||||
>
|
>
|
||||||
@ -1103,8 +1116,8 @@ const CandidateProfile: React.FC = () => {
|
|||||||
|
|
||||||
{/* Add Question Dialog */}
|
{/* Add Question Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={questionDialog}
|
open={editMode['questions']}
|
||||||
onClose={(): void => setQuestionDialog(false)}
|
onClose={(): void => setEditMode({ ...editMode, questions: false })}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
fullWidth
|
||||||
fullScreen={isMobile}
|
fullScreen={isMobile}
|
||||||
@ -1135,7 +1148,7 @@ const CandidateProfile: React.FC = () => {
|
|||||||
label="Question"
|
label="Question"
|
||||||
value={newQuestion.question || ''}
|
value={newQuestion.question || ''}
|
||||||
onChange={(e): void => setNewQuestion({ ...newQuestion, question: e.target.value })}
|
onChange={(e): void => setNewQuestion({ ...newQuestion, question: e.target.value })}
|
||||||
placeholder="What would you potential employers to ask about you?"
|
placeholder="What would should potential employers ask about you? For example, 'What are your strengths?'"
|
||||||
size={isMobile ? 'small' : 'medium'}
|
size={isMobile ? 'small' : 'medium'}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -1181,7 +1194,9 @@ const CandidateProfile: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={(): void => setQuestionDialog(false)}
|
onClick={(): void => {
|
||||||
|
setEditMode({ ...editMode, questions: false });
|
||||||
|
}}
|
||||||
fullWidth={isMobile}
|
fullWidth={isMobile}
|
||||||
size={isMobile ? 'small' : 'medium'}
|
size={isMobile ? 'small' : 'medium'}
|
||||||
>
|
>
|
||||||
|
@ -1258,7 +1258,7 @@ async def get_candidates(
|
|||||||
candidates_list = [
|
candidates_list = [
|
||||||
c
|
c
|
||||||
for c in candidates_list
|
for c in candidates_list
|
||||||
if c.is_public or (current_user.userType != UserType.GUEST and c.id == current_user.id)
|
if c.is_public or (current_user.user_type != UserType.GUEST and c.id == current_user.id)
|
||||||
]
|
]
|
||||||
|
|
||||||
paginated_candidates, total = filter_and_paginate(candidates_list, page, limit, sortBy, sortOrder, filter_dict)
|
paginated_candidates, total = filter_and_paginate(candidates_list, page, limit, sortBy, sortOrder, filter_dict)
|
||||||
|
@ -471,26 +471,6 @@ async def create_job_from_file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{job_id}")
|
|
||||||
async def get_job(job_id: str = Path(...), database: RedisDatabase = Depends(get_database)):
|
|
||||||
"""Get a job by ID"""
|
|
||||||
try:
|
|
||||||
job_data = await database.get_job(job_id)
|
|
||||||
if not job_data:
|
|
||||||
return JSONResponse(status_code=404, content=create_error_response("NOT_FOUND", "Job not found"))
|
|
||||||
|
|
||||||
# Increment view count
|
|
||||||
job_data["views"] = job_data.get("views", 0) + 1
|
|
||||||
await database.set_job(job_id, job_data)
|
|
||||||
|
|
||||||
job = Job.model_validate(job_data)
|
|
||||||
return create_success_response(job.model_dump(by_alias=True))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Get job error: {e}")
|
|
||||||
return JSONResponse(status_code=500, content=create_error_response("FETCH_ERROR", str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def get_jobs(
|
async def get_jobs(
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
@ -501,6 +481,7 @@ async def get_jobs(
|
|||||||
database: RedisDatabase = Depends(get_database),
|
database: RedisDatabase = Depends(get_database),
|
||||||
):
|
):
|
||||||
"""Get paginated list of jobs"""
|
"""Get paginated list of jobs"""
|
||||||
|
logger.info("📄 Fetching jobs...")
|
||||||
try:
|
try:
|
||||||
filter_dict = None
|
filter_dict = None
|
||||||
if filters:
|
if filters:
|
||||||
@ -531,9 +512,11 @@ async def search_jobs(
|
|||||||
filters: Optional[str] = Query(None),
|
filters: Optional[str] = Query(None),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
limit: int = Query(20, ge=1, le=100),
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
database: RedisDatabase = Depends(get_database),
|
database: RedisDatabase = Depends(get_database),
|
||||||
):
|
):
|
||||||
"""Search jobs"""
|
"""Search jobs"""
|
||||||
|
logger.info("🔍 Searching jobs...")
|
||||||
try:
|
try:
|
||||||
filter_dict = {}
|
filter_dict = {}
|
||||||
if filters:
|
if filters:
|
||||||
@ -568,6 +551,26 @@ async def search_jobs(
|
|||||||
return JSONResponse(status_code=400, content=create_error_response("SEARCH_FAILED", str(e)))
|
return JSONResponse(status_code=400, content=create_error_response("SEARCH_FAILED", str(e)))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{job_id}")
|
||||||
|
async def get_job(job_id: str = Path(...), database: RedisDatabase = Depends(get_database)):
|
||||||
|
"""Get a job by ID"""
|
||||||
|
try:
|
||||||
|
job_data = await database.get_job(job_id)
|
||||||
|
if not job_data:
|
||||||
|
return JSONResponse(status_code=404, content=create_error_response("NOT_FOUND", "Job not found"))
|
||||||
|
|
||||||
|
# Increment view count
|
||||||
|
job_data["views"] = job_data.get("views", 0) + 1
|
||||||
|
await database.set_job(job_id, job_data)
|
||||||
|
|
||||||
|
job = Job.model_validate(job_data)
|
||||||
|
return create_success_response(job.model_dump(by_alias=True))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Get job error: {e}")
|
||||||
|
return JSONResponse(status_code=500, content=create_error_response("FETCH_ERROR", str(e)))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{job_id}")
|
@router.delete("/{job_id}")
|
||||||
async def delete_job(
|
async def delete_job(
|
||||||
job_id: str = Path(...), admin_user=Depends(get_current_admin), database: RedisDatabase = Depends(get_database)
|
job_id: str = Path(...), admin_user=Depends(get_current_admin), database: RedisDatabase = Depends(get_database)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user