recruitment web first commit

This commit is contained in:
Pranay 2025-08-07 12:43:43 +05:30
parent 79d14a2023
commit e88f7da488
35 changed files with 22802 additions and 0 deletions

View File

@ -0,0 +1,2 @@
from . import controllers
from . import models

View File

@ -0,0 +1,50 @@
# __manifest__.py
{
'name': 'Recruitment web app',
'version': '18.0',
'category': 'Tools',
"author": "FTPROTECH PVT LTD",
"website": "https://www.ftprotech.in/",
'summary': 'Extracts the information of candidates from the resumes and creates applications in recruitment.',
'depends': ['base', 'web', 'web_editor', 'sms', 'hr_recruitment', 'hr_recruitment_extended', 'base_setup','website_hr_recruitment_extended'],
'assets': {
'web.assets_frontend': [
'hr_recruitment_web_app/static/lib/ckeditor/ckeditor.js',
'hr_recruitment_web_app/static/src/js/ats.js',
'hr_recruitment_web_app/static/src/js/job_requests.js',
'hr_recruitment_web_app/static/src/js/applicants.js',
'hr_recruitment_web_app/static/src/css/candidate.css',
#
#
'hr_recruitment_web_app/static/src/css/colors.css',
'hr_recruitment_web_app/static/src/css/ats.css',
'hr_recruitment_web_app/static/src/css/list.css',
'hr_recruitment_web_app/static/src/css/content.css',
'hr_recruitment_web_app/static/src/css/applicants.css',
'hr_recruitment_web_app/static/src/css/jd.css',
# 'hr_recruitment_web_application/static/src/css/applicants_details.css',
# 'hr_recruitment_web_application/static/src/css/ats_candidate.css',
],
},
'data': [
"security/ir.model.access.csv",
'views/recruitmnet_doc_upload_wizard.xml',
'views/hr_candidate.xml',
'views/res_config_view.xml',
'views/main.xml',
'views/recruitment.xml',
'views/jd.xml',
'views/applicants.xml',
'views/candidate.xml',
],
'images': ['static/description/banner.png'],
'external_dependencies': {
'python': ['pytesseract', 'pdf2image', 'pypdf'],
},
'installable': True,
'application': True,
}

View File

@ -0,0 +1 @@
from . import web_recruitment

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
from . import recruitment_doc_upload_wizard
from . import res_config_settings

View File

@ -0,0 +1,216 @@
from odoo import models, fields, api
from pypdf import PdfReader
from datetime import datetime
import base64
import re
from dateutil.relativedelta import relativedelta
import logging
import requests
from io import BytesIO
from pdf2image import convert_from_bytes
from PIL import Image
import pytesseract
import json
from docx import Document
# import binascii
from odoo.tools.mimetypes import guess_mimetype
_logger = logging.getLogger(__name__)
class RecruitmentDocUploadWizard(models.TransientModel):
_name = 'recruitment.doc.upload.wizard'
_description = 'Recruitment Document Upload Wizard'
# Define the fields in the wizard
name = fields.Char("Name")
json_data = fields.Text("Json Data")
file_name = fields.Char('File Name')
file_data = fields.Binary('File Data', required=True)
active = fields.Boolean(default=True)
mimetype = fields.Char(string="Type", readonly=True)
file_html_text = fields.Html()
def compute_mimetype(self):
for record in self:
record.mimetype = ''
if record.file_data:
try:
# Fix padding
padded_data = record.file_data + b'=' * (-len(record.file_data) % 4)
binary = base64.b64decode(padded_data)
record.mimetype = guess_mimetype(binary)
except Exception:
record.mimetype = 'Invalid base64'
def action_upload(self):
# Implement the logic for file upload here
# You can use the fields file_data, file_name, etc., to save the data in the desired model
pass
def action_fetch_json(self):
for record in self:
record.compute_mimetype()
if not record.file_data:
record.json_data = "No file content provided."
continue
binary = base64.b64decode(record.file_data)
file_type = record.mimetype
text_content = ""
try:
if file_type == "application/pdf":
try:
pdf_reader = PdfReader(BytesIO(binary))
for page in pdf_reader.pages:
page_text = page.extract_text()
text_content += page_text
if not text_content:
images = convert_from_bytes(binary, dpi=300)
extracted_text = []
for image in images:
text = pytesseract.image_to_string(image)
extracted_text.append(text)
text_content = "\n".join(extracted_text)
except Exception as e:
_logger.error("Error reading PDF: %s", str(e))
text_content = ""
elif file_type in ["image/png", "image/jpeg", "image/jpg"]:
try:
image = Image.open(BytesIO(binary))
text_content = pytesseract.image_to_string(image)
except Exception as e:
_logger.error("Error processing image: %s", str(e))
text_content = ""
elif file_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
try:
doc = Document(BytesIO(binary))
text_content = ''
for section in doc.sections:
header = section.header
for paragraph in header.paragraphs:
text_content += paragraph.text + '\n'
for paragraph in doc.paragraphs:
text_content += paragraph.text + '\n'
except Exception as e:
_logger.error("Error processing DOCX: %s", str(e))
text_content = ""
else:
_logger.error("Unsupported file type: %s", file_type)
record.json_data = f"Unsupported file type: {file_type}"
continue
record.file_html_text = text_content
json_response = record.get_json_from_model(text_content)
import pdb
pdb.set_trace()
if json_response and "choices" in json_response and len(json_response["choices"]) > 0:
message_content = json_response["choices"][0].get("message", {}).get("content", "")
if message_content:
match = re.search(r'```json\n(.*?)\n```', message_content, re.DOTALL)
if match:
clean_json_str = match.group(1).strip() # Extract JSON content
try:
parsed_json = json.loads(clean_json_str)
record.json_data = json.dumps(parsed_json, indent=4)
file_name = parsed_json.get("name", "")
if file_name:
self.write({'file_name': file_name})
except json.JSONDecodeError as e:
_logger.error("Error parsing JSON: %s", str(e))
record.json_data = "Error parsing JSON"
else:
_logger.error("No valid JSON found in the content")
record.json_data = "No valid JSON format found"
else:
_logger.error("No message content found in the response")
record.json_data = "No message content found"
else:
_logger.error("No valid response or choices in the API response")
record.json_data = "No valid JSON data received."
except Exception as e:
_logger.error("Unexpected error during OCR processing: %s", str(e))
record.json_data = "An unexpected error occurred during file processing."
_logger.info("Stored JSON data for file: %s", record.file_name)
def normalize_gender(self, gender):
if gender:
return gender.replace(" ", "").lower()
return gender
def normalize_marital_status(self, marital_status):
if marital_status:
return marital_status.replace(" ", "").lower()
return marital_status
def parse_experience(self, experience_str):
years = 0
months = 0
year_match = re.search(r'(\d+)\s*[\+]*\s*years?', experience_str, re.IGNORECASE)
month_match = re.search(r'(\d+)\s*months?', experience_str, re.IGNORECASE)
if year_match:
years = int(year_match.group(1))
if month_match:
months = int(month_match.group(1))
return years, months
def get_json_from_model(self, text_content):
print(text_content)
api_url = "https://api.together.xyz/v1/chat/completions"
together_api_key = self.env['ir.config_parameter'].sudo().get_param('hr_recruitment_web_app.together_api_key')
headers = {
'Authorization': 'Bearer %s' % together_api_key,
'Content-Type': 'application/json',
}
current_date = datetime.now()
previous_month_date = current_date - relativedelta(months=1)
previous_month_year = previous_month_date.strftime("%B %Y")
payload = {
"messages": [
{
"role": "system",
"content": "provide the json data from the above content for below fields---\n\nname-- particularly the full name of the candidate\nskills-- the skills of the candidate mentioned in the text specifically under skills section (add both soft and technical skills in this). \nemail -- the contact email of the candidate \nphone -- contact number of candidate usually a 10-12 number digits\ndegree-- the degree or qualification of the candidate mentioned in resume (only the name of the degrees or qualifications and not the whole details) "
f"\n experience in years and months in the format: Title of the experience (type of experience) (from month/year to month/year) -> years and months (If the end date is marked as 'present' or 'till now', assume today's date is {previous_month_year} and calculate the months also properly). \n"
"\n Total Experience (Non-Overlapping) : in years and months \n"
"\n location-- search for the location or place mentioned in the resume where the candidate belong to. \n gender-- the gender of the candidate if mentioned. \ndate_of_birth-- the date of birth of the candidate in the format-%d/%m/%Y \nmarital_status-- the marital status of the candidate \n languages-- the languages known by the candidate mentioned in resume(not technical languages but the spoken ones specifically mentioned under languages section)\n"
},
{
"role": "user",
"content": text_content
}
],
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
}
try:
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
try:
json_response = response.json()
return json_response
except ValueError as e:
_logger.error("Error parsing JSON: %s", str(e))
return {}
else:
_logger.error("Error in API call: %s", response.text)
return {}
except Exception as e:
_logger.error("Exception during API call: %s", str(e))
return {}

View File

@ -0,0 +1,7 @@
from odoo import api, fields, models, _
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
together_api_key = fields.Char(config_parameter='hr_recruitment_web_app.together_api_key', string="Together API key")

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_recruitment_doc_upload_wizard_user,recruitment.doc.upload.wizard user,model_recruitment_doc_upload_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_recruitment_doc_upload_wizard_user recruitment.doc.upload.wizard user model_recruitment_doc_upload_wizard base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -0,0 +1,498 @@
@import url('colors.css');
/* ========= application creation css ========= */
/* ===== Application Modal Styles ===== */
.application-creation-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.application-creation-modal.show {
display: flex;
opacity: 1;
}
.application-creation-modal .application-creation-content {
background-color: var(--white);
width: 85%;
max-width: 1200px;
border-radius: 8px;
box-shadow: 0 5px 20px var(--shadow-dark);
transform: translateY(-20px);
transition: transform 0.3s ease;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.application-creation-modal.show .application-creation-content {
transform: translateY(0);
}
.application-creation-modal .application-creation-header {
padding: 20px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.application-creation-modal .header-icon-container {
display: flex;
align-items: center;
gap: 15px;
}
.application-creation-modal .header-icon {
font-size: 24px;
color: var(--white);
}
.application-creation-modal .application-creation-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
color: var(--white);
}
.application-creation-modal .application-creation-close {
font-size: 28px;
cursor: pointer;
transition: transform 0.2s;
color: var(--white);
background: none;
border: none;
}
.application-creation-modal .application-creation-close:hover {
transform: scale(1.2);
color: var(--gray-200);
}
.application-creation-modal .application-creation-body {
padding: 25px;
overflow-y: auto;
flex-grow: 1;
}
.application-creation-modal .form-section {
background-color: var(--white);
border-radius: 6px;
padding: 20px;
box-shadow: 0 2px 5px var(--shadow-color);
border-left: 4px solid var(--secondary-purple);
margin-bottom: 20px;
}
.application-creation-modal .section-title {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-primary);
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.application-creation-modal .section-title i {
color: var(--secondary-purple);
}
.application-creation-modal .form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.application-creation-modal .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.application-creation-modal .form-group label {
font-weight: 500;
color: var(--text-secondary);
font-size: 14px;
}
.application-creation-modal .form-input,
.application-creation-modal .form-select,
.application-creation-modal .form-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: var(--white);
color: var(--text-primary);
}
.application-creation-modal .form-input:focus,
.application-creation-modal .form-select:focus,
.application-creation-modal .form-textarea:focus {
border-color: var(--secondary-purple);
box-shadow: 0 0 0 3px var(--primary-blue-light);
outline: none;
}
/* Checkbox styles */
.application-creation-modal .same-as-current {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.application-creation-modal .same-as-current input[type="checkbox"] {
width: auto;
}
/* Education entries */
.application-creation-modal .education-entries {
margin-bottom: 15px;
}
.application-creation-modal .education-entry {
position: relative;
padding: 15px;
background-color: var(--gray-50);
border-radius: 6px;
margin-bottom: 15px;
border: 1px solid var(--border-light);
}
.application-creation-modal .remove-education {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: var(--danger);
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 5px;
}
.application-creation-modal .remove-education:hover {
text-decoration: underline;
}
.application-creation-modal .add-education {
background-color: var(--primary-blue);
color: var(--white);
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 20px;
transition: background-color 0.3s;
}
.application-creation-modal .add-education:hover {
background-color: var(--primary-blue-dark);
}
/* Skills section */
.application-creation-modal .skills-container {
margin-top: 20px;
}
/* Upload area */
/* Resume Section */
.application-creation-modal .resume-upload-container {
display: flex;
gap: 20px;
}
.application-creation-modal .upload-area {
flex: 1;
border: 2px dashed var(--border-color);
border-radius: 6px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
max-width: 30%;
}
.application-creation-modal .upload-area:hover {
border-color: var(--primary-blue);
background-color: var(--gray-50);
}
.application-creation-modal .upload-icon {
font-size: 40px;
color: var(--primary-blue);
margin-bottom: 10px;
}
.application-creation-modal .upload-area h5 {
margin: 0 0 5px;
color: var(--text-primary);
}
.application-creation-modal .upload-area p {
margin: 0;
color: var(--text-muted);
font-size: 14px;
}
.application-creation-modal .resume-preview {
flex: 1;
border: 1px solid var(--border-light);
border-radius: 6px;
padding: 15px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--gray-50);
}
.application-creation-modal .resume-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
height: 100%;
}
.application-creation-modal .resume-preview-placeholder i {
font-size: 40px;
margin-bottom: 10px;
color: var(--primary-blue);
}
.application-creation-modal .btn-danger {
background-color: var(--danger);
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.application-creation-modal .btn-danger:hover {
background-color: #dc3545;
}
/* Additional attachments */
.application-creation-modal .additional-attachments {
margin-top: 20px;
}
.application-creation-modal .attachments-list {
margin: 10px 0;
}
.application-creation-modal .add-attachment {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.application-creation-modal .add-attachment:hover {
background-color: var(--primary-blue-light);
}
/* Form actions */
.application-creation-modal .form-actions {
position: sticky;
bottom: 0;
background: var(--white);
padding: 15px 25px;
border-top: 1px solid var(--border-light);
z-index: 100;
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.application-creation-modal .btn-cancel {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--gray-100);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.application-creation-modal .btn-cancel:hover {
background-color: var(--gray-200);
}
.application-creation-modal .btn-application-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.application-creation-modal .btn-application-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
.application-creation-modal .footer-right {
display: flex;
gap: 10px;
}
/* SELECT2 custom styles */
.application-creation-modal .select2-container {
z-index: 10000 !important;
width: 100% !important;
}
.application-creation-modal .select2-container--default .select2-selection--multiple,
.application-creation-modal .select2-container--default .select2-selection--single {
border: 1px solid var(--border-color) !important;
border-radius: 4px !important;
min-height: 40px;
background-color: var(--white);
padding: 6px 8px !important;
flex-wrap: wrap !important;
gap: 4px;
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: var(--primary-blue) !important;
border: none !important;
color: var(--white) !important;
padding: 2px 8px !important;
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--white) !important;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}
.application-creation-modal .select2-container--default .select2-selection__rendered {
display: flex !important;
align-items: center !important;
vertical-align: middle !important;
line-height: normal !important;
color: var(--text-primary);
}
.application-creation-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
width: 100% !important;
padding: 2px 5px !important;
overflow: visible !important;
box-sizing: border-box !important;
}
.application-creation-modal .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary-blue) !important;
color: var(--white) !important;
}
/* Scrollbar styling */
.application-creation-modal .application-creation-body::-webkit-scrollbar {
width: 8px;
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-track {
background: var(--gray-100);
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-thumb {
background: var(--primary-blue);
border-radius: 4px;
}
.application-creation-modal .application-creation-body::-webkit-scrollbar-thumb:hover {
background: var(--primary-blue-dark);
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.application-creation-modal .application-creation-content {
width: 90%;
}
}
@media (max-width: 768px) {
.application-creation-modal .application-creation-content {
width: 95%;
height: 95vh;
}
.application-creation-modal .form-grid {
grid-template-columns: 1fr;
}
}
/* Special cases */
.application-creation-modal .marital-anniversary {
display: none;
}
.application-creation-modal #application-marital[value="married"] ~ .marital-anniversary {
display: flex;
}
.application-creation-modal .form-section.profile-section:first-child {
margin-top: 0;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}

View File

@ -0,0 +1,217 @@
@import url('colors.css');
/* ===== ATS Custom Layout Reset ===== */
body.ats-app {
margin: 0;
padding: 0;
font-family: "Segoe UI", sans-serif;
background-color: var(--body-bg);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
/* ===== Header ===== */
.ats-app .main-header {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
font-size: 1.8rem;
font-weight: 600;
padding: 1rem;
position: relative; /* For absolute positioning of toggle button */
}
/* Logo and title alignment */
.ats-app .main-header img {
height: 40px;
width: auto;
}
.ats-app .main-header span {
display: flex;
align-items: center;
height: 40px; /* Match logo height */
}
/* ===== Layout Container ===== */
.ats-app .layout-container {
display: flex;
height: 100%; /* Adjust for header height */
overflow: hidden;
}
/* ===== Sidebar ===== */
.ats-app .sidebar {
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
padding: 1rem 1rem 2rem;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
transition: all 0.4s ease-in-out;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Expanded Sidebar */
.ats-app .sidebar.expanded {
width: 240px; /* Use fixed width for better consistency */
min-width: 240px;
}
/* Collapsed Sidebar */
.ats-app .sidebar.collapsed {
width: 60px;
min-width: 60px;
padding: 1rem 0.4rem 2rem;
}
/* Hide content in collapsed state */
.ats-app .sidebar.collapsed .menu-list,
.ats-app .sidebar.collapsed .main-header span {
display: none;
}
/* ===== Sidebar Menu ===== */
.ats-app .menu-list {
list-style: none;
padding-top: 1rem;
margin: 2rem 0;
flex-grow: 1;
}
.ats-app .list-title {
color: var(--text-secondary);
font-size: 0.9rem;
}
.ats-app .menu-list li {
margin-bottom: 1.2rem;
}
.ats-app .menu-list a {
display: block;
color: var(--sidebar-text);
text-decoration: none;
padding: 0.7rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
font-size: 1.1rem;
}
.ats-app .menu-list a:hover {
background-color: #DBEDFE;
color: #0061FF;
transform: translateX(6px);
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
font-weight: 600;
letter-spacing: 0.5px;
}
/* ===== Toggle Button ===== */
.ats-app .toggle-btn {
position: absolute;
top: 1.5rem;
right: -12px;
background-color: var(--sidebar-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 0.5rem;
cursor: pointer;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 10;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.ats-app .toggle-btn:hover {
background-color: #e1e5eb;
}
/* ===== Main Content Area ===== */
.ats-app .content-area {
flex: 1;
padding: 2rem;
background-color: var(--content-bg);
overflow-y: auto;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02);
border-left: 1px solid #dcdde1;
min-width: 0;
}
/* ===== Responsive Design ===== */
@media (max-width: 1024px) {
.ats-app .layout-container {
flex-direction: column;
height: auto;
}
.ats-app .sidebar,
.ats-app .sidebar.expanded,
.ats-app .sidebar.collapsed {
width: 100%;
padding: 1rem;
text-align: center;
}
.ats-app .sidebar {
overflow: hidden;
max-height: 15%; /* expanded */
transition: all 0.4s ease-in-out;
}
.ats-app .sidebar.collapsed {
max-height: 1%;
padding: 0;
}
.ats-app .sidebar.collapsed .menu-list,
.ats-app .sidebar.collapsed .main-header h1 {
display: none;
}
.ats-app .menu-list {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.menu-list i {
margin-right: 8px;
font-size: 1.1rem;
vertical-align: middle;
}
.ats-app .menu-list li {
margin-bottom: 0;
}
.ats-app .menu-list a {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.ats-app .toggle-btn {
position: relative;
top: auto;
right: auto;
transform: none;
margin: 1rem auto 0 auto;
display: block;
border-radius: 6px;
}
.ats-app .content-area {
padding: 1rem;
border-left: none;
}
}

View File

@ -0,0 +1,667 @@
@import url('colors.css');
/* ====== candidate creation template ======= */
/* ===== Candidate Form Styles ===== */
.candidate-form-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.candidate-form-modal.show {
display: flex;
opacity: 1;
}
.candidate-form-modal .candidate-form-content {
background-color: var(--white);
width: 90%;
max-width: 1400px;
border-radius: 8px;
box-shadow: 0 5px 20px var(--shadow-dark);
transform: translateY(-20px);
transition: transform 0.3s ease;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.candidate-form-modal.show .candidate-form-content {
transform: translateY(0);
}
.candidate-form-modal .candidate-form-header {
padding: 20px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.candidate-form-modal .header-icon-container {
display: flex;
align-items: center;
gap: 15px;
}
.candidate-form-modal .header-icon {
font-size: 24px;
color: var(--white);
}
.candidate-form-modal .candidate-form-header h3 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
color: var(--white);
}
.candidate-form-modal .candidate-form-close {
font-size: 28px;
cursor: pointer;
transition: transform 0.2s;
color: var(--white);
background: none;
border: none;
}
.candidate-form-modal .candidate-form-close:hover {
transform: scale(1.2);
color: var(--gray-200);
}
.candidate-form-modal .candidate-form-body {
padding: 25px;
overflow-y: auto;
flex-grow: 1;
}
/* Header Section with Avatar */
.candidate-form-modal .header-section {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-light);
}
.candidate-form-modal .avatar-container {
display: flex;
align-items: center;
gap: 20px;
width: 100%;
}
.candidate-form-modal .candidate-avatar {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--primary-blue);
box-shadow: 0 3px 10px var(--shadow-color);
margin-right: 20px;
}
.candidate-form-modal .candidate-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.candidate-form-modal .avatar-upload {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
text-align: center;
padding: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.candidate-form-modal .avatar-upload:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.candidate-form-modal .avatar-upload i {
margin-right: 5px;
}
.candidate-form-modal .avatar-upload input {
display: none;
}
.candidate-form-modal .basic-info {
flex-grow: 1;
}
/* Button Box Section */
.candidate-form-modal .button-box {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.candidate-form-modal .stat-button {
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
background-color: var(--gray-50);
border: 1px solid var(--border-light);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.candidate-form-modal .stat-button:hover {
background-color: var(--gray-100);
transform: translateY(-2px);
box-shadow: 0 2px 5px var(--shadow-color);
}
.candidate-form-modal .stat-button i {
font-size: 24px;
color: var(--primary-blue);
}
.candidate-form-modal .stat-info {
display: flex;
flex-direction: column;
}
.candidate-form-modal .stat-value {
font-weight: 600;
color: var(--text-primary);
}
.candidate-form-modal .stat-text {
font-size: 12px;
color: var(--text-muted);
}
/* Form Sections */
.candidate-form-modal .form-section {
background-color: var(--white);
border-radius: 6px;
padding: 20px;
box-shadow: 0 2px 5px var(--shadow-color);
border-left: 4px solid var(--secondary-purple);
margin-bottom: 20px;
}
.candidate-form-modal .section-title {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-primary);
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.candidate-form-modal .section-title i {
color: var(--secondary-purple);
}
.candidate-form-modal .form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.candidate-form-modal .form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.candidate-form-modal .form-group label {
font-weight: 500;
color: var(--text-secondary);
font-size: 14px;
}
.candidate-form-modal .form-input,
.candidate-form-modal .form-select,
.candidate-form-modal .form-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: var(--white);
color: var(--text-primary);
}
.candidate-form-modal .form-input:focus,
.candidate-form-modal .form-select:focus,
.candidate-form-modal .form-textarea:focus {
border-color: var(--secondary-purple);
box-shadow: 0 0 0 3px var(--primary-blue-light);
outline: none;
}
/* Resume Section */
.candidate-form-modal .resume-upload-container {
display: flex;
gap: 20px;
}
.candidate-form-modal .resume-upload-area {
flex: 1;
border: 2px dashed var(--border-color);
border-radius: 6px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
max-width: 30%;
}
.candidate-form-modal .resume-upload-area:hover {
border-color: var(--primary-blue);
background-color: var(--gray-50);
}
.candidate-form-modal .upload-icon {
font-size: 40px;
color: var(--primary-blue);
margin-bottom: 10px;
}
.candidate-form-modal .resume-upload-area h5 {
margin: 0 0 5px;
color: var(--text-primary);
}
.candidate-form-modal .resume-upload-area p {
margin: 0;
color: var(--text-muted);
font-size: 14px;
}
.candidate-form-modal .resume-preview {
flex: 1;
border: 1px solid var(--border-light);
border-radius: 6px;
padding: 15px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--gray-50);
}
.candidate-form-modal .resume-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
height: 100%;
}
.candidate-form-modal .resume-preview-placeholder i {
font-size: 40px;
margin-bottom: 10px;
color: var(--primary-blue);
}
.candidate-form-modal .btn-danger {
background-color: var(--danger);
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.candidate-form-modal .btn-danger:hover {
background-color: #dc3545;
}
/* Make sure dropzone doesn't interfere with child elements */
.candidate-form-modal .resume-upload-area > * {
pointer-events: none;
}
.candidate-form-modal .resume-upload-area input {
pointer-events: auto;
}
.candidate-form-modal #resume-iframe {
width: 100%;
height: 500px;
border: none;
}
.candidate-form-modal #resume-image {
max-width: 100%;
max-height: 500px;
object-fit: contain;
}
.candidate-form-modal #unsupported-format {
text-align: center;
color: var(--danger);
}
.candidate-form-modal #unsupported-format i {
font-size: 40px;
margin-bottom: 10px;
}
.candidate-form-modal #download-resume {
display: inline-flex;
align-items: center;
gap: 5px;
}
/* Skills Section */
.candidate-form-modal .skills-container {
margin-top: 15px;
}
.candidate-form-modal .skills-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.candidate-form-modal .skill-tag {
background-color: var(--primary-blue);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.candidate-form-modal .skill-tag i {
cursor: pointer;
font-size: 12px;
}
.candidate-form-modal .btn-add-skill {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.candidate-form-modal .btn-add-skill:hover {
background-color: var(--primary-blue-light);
}
/* Notebook Tabs */
.candidate-form-modal .notebook-tabs {
display: flex;
border-bottom: 1px solid var(--border-light);
margin-bottom: 20px;
}
.candidate-form-modal .tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.candidate-form-modal .tab.active {
border-bottom-color: var(--primary-blue);
color: var(--primary-blue);
font-weight: 600;
}
.candidate-form-modal .tab:hover:not(.active) {
background-color: var(--gray-50);
}
.candidate-form-modal .notebook-content {
margin-top: 15px;
}
.candidate-form-modal .tab-content {
display: none;
}
.candidate-form-modal .tab-content.active {
display: block;
}
/* Sub-sections in notebook */
.candidate-form-modal .sub-section {
margin-bottom: 30px;
}
.candidate-form-modal .sub-section-title {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-size: 16px;
margin-bottom: 15px;
}
.candidate-form-modal .sub-section-title i {
color: var(--secondary-purple);
}
/* Form Actions */
.candidate-form-modal .form-actions {
position: sticky;
bottom: 0;
background: var(--white);
padding: 15px 25px;
border-top: 1px solid var(--border-light);
z-index: 100;
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.candidate-form-modal .btn-cancel {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--gray-100);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.candidate-form-modal .btn-cancel:hover {
background-color: var(--gray-200);
}
.candidate-form-modal .btn-candidate-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.candidate-form-modal .btn-candidate-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
.candidate-form-modal .footer-right {
display: flex;
gap: 10px;
}
.candidate-form-modal .select2-container {
z-index: 10000 !important;
width: 100% !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple,
.candidate-form-modal .select2-container--default .select2-selection--single {
border: 1px solid var(--border-color) !important;
border-radius: 4px !important;
min-height: 40px;
background-color: var(--white);
padding: 5px 5px 0 5px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: var(--primary-blue) !important;
border: none !important;
color: var(--white) !important;
padding: 2px 8px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
color: var(--white) !important;
}
.application-creation-modal .select2-dropdown {
border: 1px solid var(--border-color) !important;
box-shadow: 0 2px 5px var(--shadow-color) !important;
}
.candidate-form-modal .select2-container--default .select2-selection__rendered {
display: flex !important;
align-items: center !important;
vertical-align: middle !important;
line-height: normal !important;
color: var(--text-primary);
padding: 2px 5px !important;
}
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
width: 100% !important;
padding: 2px 5px !important;
overflow: visible !important;
box-sizing: border-box !important;
}
.candidate-form-modal .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary-blue) !important;
color: var(--white) !important;
}
/* Scrollbar styling */
.candidate-form-modal .candidate-form-body::-webkit-scrollbar {
width: 8px;
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-track {
background: var(--gray-100);
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb {
background: var(--primary-blue);
border-radius: 4px;
}
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb:hover {
background: var(--primary-blue-dark);
}
.candidate-form-modal .btn-candidate-primary {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
color: var(--white);
border: none;
}
.candidate-form-modal .btn-candidate-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.candidate-form-modal .candidate-form-content {
width: 95%;
}
.candidate-form-modal .resume-upload-container {
flex-direction: column;
}
}
@media (max-width: 768px) {
.candidate-form-modal .candidate-form-content {
width: 98%;
height: 95vh;
}
.candidate-form-modal .form-grid {
grid-template-columns: 1fr;
}
.candidate-form-modal .avatar-container {
flex-direction: column;
text-align: center;
}
.candidate-form-modal .button-box {
justify-content: center;
}
.candidate-form-modal .stat-button {
min-width: 150px;
}
}

View File

@ -0,0 +1,66 @@
:root {
/* Primary Colors */
--primary-blue: #3498db;
--primary-blue-dark: #2980b9;
--primary-blue-light: #e8f0ff;
/* Secondary Colors */
--secondary-purple: #6f42c1;
--secondary-green: #28a745;
--secondary-red: #dc3545;
--secondary-yellow: #ffc107;
/* Grayscale */
--white: #ffffff;
--gray-100: #f8f9fa;
--gray-200: #e9ecef;
--gray-300: #dee2e6;
--gray-400: #ced4da;
--gray-500: #adb5bd;
--gray-600: #6c757d;
--gray-700: #495057;
--gray-800: #343a40;
--gray-900: #212529;
--black: #000000;
/* Semantic Colors */
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
/* Background Colors */
--body-bg: #f5f6fa;
--sidebar-bg: #FAFCFF;
--create-model-bg: #FAFCFF;
--content-bg: #E3E9EF;
--active-search-bg: #ffffff;
--active-search-hover-bg: #f0f0f0;
--add-btn-bg: #3498db;
--add-btn-hover-bg: #2980b9;
--side-panel-bg: #FAFCFF;
--side-panel-item-hover: #f0f8ff;
--side-panel-item-selected: #d7eaff;
/* Text Colors */
--text-primary: #2f3542;
--text-secondary: #4B5865;
--text-muted: #6c757d;
--sidebar-text: #0F1419;
--active-search-text: #333;
--add-btn-color: #ffffff;
/* Border Colors */
--border-color: #dcdde1;
--border-light: #e0e0e0;
/* Shadow Colors */
--shadow-color: rgba(0, 0, 0, 0.1);
--shadow-dark: rgba(0, 0, 0, 0.2);
/* Status Colors */
--status-new: #3498db;
--status-interview: #f39c12;
--status-hired: #2ecc71;
--status-rejected: #e74c3c;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,708 @@
@import url('colors.css');
/* ===== Job List View Styling ===== */
.ats-list-container {
display: flex;
flex-direction: column;
height: 99%;
width: 100%;
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 8px var(--shadow-color);
background-color: var(--body-bg);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.ats-list-container .ats-list-search {
flex: 0 0 auto;
padding: 12px;
background-color: var(--gray-100);
border-bottom: 1px solid var(--border-light);
}
.ats-list-container .ats-list-search input {
width: 100%;
padding: 10px 14px;
font-size: 14px;
border: 1px solid var(--gray-300);
border-radius: 6px;
outline: none;
transition: border-color 0.3s ease;
background-color: var(--white);
color: var(--text-primary);
}
.ats-list-container .ats-list-search input:focus {
border-color: var(--primary-blue);
}
.ats-list-container .ats-list-body {
flex: 1;
display: flex;
height: calc(100vh - 70px); /* header + search box approx height */
overflow: hidden;
position: relative;
}
.ats-list-container .ats-actions-header {
padding: 10px 15px;
background-color: var(--content-bg);
border-radius: 6px;
border-left: 4px solid var(--primary-blue);
display: flex;
align-items: center;
}
.ats-list-container .ats-actions-header .section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.ats-list-container .ats-actions-header .btn {
font-size: 14px;
font-weight: 500;
padding: 6px 15px;
border-radius: 4px;
transition: all 0.3s ease;
}
.ats-list-container .ats-actions-header #activeRecords {
margin-right: auto;
background-color: var(--active-search-bg);
color: var(--active-search-text);
border: 1px solid var(--gray-300);
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
box-shadow: 0 2px 4px var(--shadow-color);
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.ats-list-container .job-actions-header #activeRecords:hover {
background-color: var(--active-search-hover-bg);
color: var(--active-search-text);
box-shadow: 0 4px 8px var(--shadow-dark);
}
/* Button Styles */
.ats-list-container .ats-actions-header .add-create-btn {
margin-left: auto;
background-color: var(--add-btn-bg);
color: var(--add-btn-color);
border: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(52, 152, 219, 0.2);
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.ats-list-container .ats-actions-header .add-create-btn:hover {
background-color: var(--add-btn-hover-bg);
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(41, 128, 185, 0.3);
}
.ats-list-container .ats-actions-header .add-create-btn .plus-icon {
font-size: 20px;
font-weight: bold;
line-height: 1;
display: inline-block;
transition: transform 0.3s ease;
}
.ats-list-container .ats-actions-header .add-create-btn:hover .plus-icon {
transform: scale(1.3);
}
/* ===== Job List Panel ===== */
.ats-list-container .ats-list-left {
width: 30%;
padding: 0;
overflow-y: auto;
background-color: var(--content-bg);
position: relative;
max-height: 100%;
display: flex;
flex-direction: column;
}
.ats-list-container .ats-list-left ul {
list-style: none;
padding: 12px 12px 20px 12px;
margin: 0;
flex: 1;
overflow-y: auto;
}
/* ===== Kanban View (Full Screen) ===== */
.ats-list-container:not(.ats-selected) .ats-list-left {
width: 100%;
display: flex;
flex-direction: column;
}
.ats-list-container:not(.ats-selected) .ats-list-left ul {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-auto-rows: 1fr; /* Equal height rows */
grid-gap: 16px;
padding: 16px;
overflow-y: auto;
width: 100%;
box-sizing: border-box;
align-content: start; /* Align items to the top */
}
.ats-list-container:not(.ats-selected) .ats-item {
padding: 16px;
margin-bottom: 0;
border: 1px solid var(--gray-200);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background-color: var(--white);
box-shadow: 0 2px 6px var(--shadow-color);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
height: auto; /* Fill the grid cell */
min-height: 150; /* Minimum height */
}
.ats-list-container:not(.ats-selected) .ats-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
background-color: var(--primary-blue);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.ats-list-container:not(.ats-selected) .ats-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px var(--shadow-dark);
border-color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item:hover::before {
transform: scaleY(1);
}
.ats-list-container:not(.ats-selected) .ats-item .ats-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
padding-right: 20px;
}
.ats-list-container:not(.ats-selected) .ats-item .ats-meta {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 12px;
display: flex;
align-items: center;
}
.ats-list-container:not(.ats-selected) .ats-item .ats-meta i {
margin-right: 6px;
color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item .ats-badges {
margin-top: auto;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.ats-list-container:not(.ats-selected) .ats-item .job-badge {
margin-right: 0;
padding: 4px 10px;
font-size: 12px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 24px;
}
.ats-list-container:not(.ats-selected) .ats-item .badge-primary {
background-color: var(--primary-blue);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-warning {
background-color: var(--warning);
color: var(--black);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-success {
background-color: var(--success);
}
.ats-list-container:not(.ats-selected) .ats-item .badge-danger {
background-color: var(--danger);
}
/* ===== List View (When Job Selected) ===== */
.ats-list-container.ats-selected .ats-item {
padding: 10px 12px;
margin-bottom: 8px;
border: 1px solid var(--gray-200);
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s, box-shadow 0.2s;
background-color: var(--sidebar-bg);
}
.ats-list-container.ats-selected .ats-item:hover {
background-color: var(--side-panel-item-hover);
box-shadow: 0 1px 4px var(--shadow-color);
}
.ats-list-container.ats-selected .ats-item.selected {
background-color: var(--side-panel-item-selected);
border: 1px solid var(--primary-blue);
}
/* ===== Job Detail Panel ===== */
.ats-list-container .ats-detail {
width: 70%;
padding: 20px;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--content-bg);
color: var(--text-primary);
position: relative;
flex: 1;
transition: all 0.3s ease;
}
.ats-list-container .ats-detail h3 {
margin-top: 0;
font-size: 20px;
color: var(--primary-blue);
}
.ats-list-container .ats-detail p {
margin: 8px 0;
line-height: 1.6;
color: var(--text-secondary);
}
.ats-list-container .ats-detail em {
color: var(--text-muted);
}
/* ===== Panel Controls ===== */
.ats-list-container .panel-controls {
position: sticky;
top: 0;
background: var(--white);
z-index: 100;
padding: 8px 0;
display: flex;
justify-content: flex-end;
}
.ats-list-container .panel-controls button {
background-color: var(--gray-200);
border: none;
color: var(--text-primary);
font-size: 14px;
margin-left: 5px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.ats-list-container .panel-controls button:hover {
background-color: var(--gray-300);
}
/* ======Job stats====== */
.ats-list-container .job-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--content-bg);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
}
.ats-list-container .job-stats-values {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.ats-list-container .close-stats {
background: none;
border: none;
font-size: 18px;
color: var(--gray-600);
cursor: pointer;
padding: 4px 8px;
transition: color 0.3s;
}
.ats-list-container .close-stats:hover {
color: var(--gray-900);
}
.ats-list-container .badge {
padding: 6px 10px;
border-radius: 14px;
font-size: 12px;
font-weight: 600;
color: var(--white);
}
.ats-list-container .stat-toggle.crossed {
text-decoration: line-through;
opacity: 0.6;
cursor: pointer;
}
.ats-list-container .stat-toggle {
cursor: pointer;
margin-right: 8px;
}
.ats-list-container .job-badges {
margin-top: 6px;
}
.ats-list-container .job-badge {
margin-right: 6px;
padding: 4px 10px;
font-size: 12px;
border-radius: 12px;
display: inline-block;
}
.ats-list-container .badge-primary { background-color: var(--primary-blue); }
.ats-list-container .badge-warning { background-color: var(--warning); color: var(--black); }
.ats-list-container .badge-success { background-color: var(--success); }
.ats-list-container .badge-danger { background-color: var(--danger); }
/* ===== Sidebar Toggle ===== */
.ats-list-container .ats-list-left {
width: 30%;
min-width: 300px;
transition: all 0.3s ease;
position: relative;
overflow-x: hidden;
}
.ats-list-container .ats-list-left.collapsed {
width: 5%;
min-width: 5%;
padding: 0;
border-right: none;
}
.ats-list-container .ats-detail {
width: 70%;
transition: all 0.3s ease;
}
.ats-list-container .ats-list-left.collapsed + .ats-detail {
width: 95%;
}
/* Toggle Button */
.ats-list-container .ats-list-toggle-btn {
position: absolute;
top: 50%;
right: -12px;
transform: translateY(-50%);
background-color: var(--gray-100);
color: var(--text-primary);
border: none;
padding: 0.5rem 0.7rem;
cursor: pointer;
border-radius: 0 6px 6px 0;
box-shadow: 0 2px 5px var(--shadow-color);
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
transition: right 0.3s ease;
}
/* Button in collapsed state */
.ats-list-container .ats-list-left.collapsed .ats-list-toggle-btn {
right: -12px;
transform: translateY(-50%);
}
.ats-list-container .ats-list-toggle-btn:hover {
background-color: var(--gray-200);
}
/* Hide content in collapsed state */
.ats-list-container .ats-list-left.collapsed > *:not(.job-list-toggle-btn) {
display: none;
}
/* ===== Layout States ===== */
/* Initial state: job list takes full width */
.ats-list-container:not(.ats-selected) .ats-list-left {
width: 100%;
}
.ats-list-container:not(.ats-selected) .ats-detail {
display: none;
}
/* When a job is selected */
.ats-list-container.ats-selected .ats-list-left {
width: 30%;
}
.ats-list-container.ats-selected .ats-detail {
width: 70%;
display: block;
overflow: hidden;
}
/* When sidebar is collapsed */
.ats-list-container.ats-selected .ats-list-left.collapsed {
width: 5%;
min-width: 5%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed + .ats-detail {
width: 95%;
}
.ats-list-container:not(.ats-selected) .ats-list-toggle-btn {
display: none;
}
.ats-list-container.ats-selected .ats-list-toggle-btn {
display: flex;
}
/* Close button styling */
.close-detail {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background: transparent;
border: none;
font-size: 1.5rem;
color: var(--gray-600);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.close-detail:hover {
background-color: var(--gray-200);
color: var(--gray-900);
}
/* Transitions for smooth resizing */
.ats-list-container .ats-list-left,
.ats-list-container .ats-detail {
transition: width 0.3s ease;
}
/* ===== Responsive Adjustments ===== */
@media (max-width: 768px) {
.ats-list-container:not(.ats-selected) .ats-list-left ul {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-gap: 12px;
padding: 12px;
}
.ats-list-container:not(.ats-selected) .ats-item {
padding: 12px;
min-height: 160px;
}
.ats-list-container.ats-selected .ats-list-left {
width: 40%;
}
.ats-list-container.ats-selected .ats-detail {
width: 60%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed {
width: 10%;
min-width: 10%;
}
.ats-list-container.ats-selected .ats-list-left.collapsed + .ats-detail {
width: 90%;
}
}
/* ===== Applicant List Styling ===== */
.ats-list-container .ats-list {
list-style: none;
padding: 12px 12px 20px 12px;
margin: 0;
flex: 1;
overflow-y: auto;
}
.ats-list-container .ats-item-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.ats-list-container .ats-info {
flex: 1;
}
.ats-list-container .ats-avatar {
margin-left: 12px;
flex-shrink: 0;
}
.ats-list-container .ats-item-image {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.ats-list-container .ats-item-initials {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--primary-blue);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
}
/* ===== Applicant Modal Styling ===== */
.applicant-detail-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.applicant-detail-modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.applicant-modal-content {
position: relative;
background-color: var(--white);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
z-index: 1001;
}
.applicant-close-modal {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
font-weight: bold;
color: var(--gray-600);
cursor: pointer;
z-index: 1002;
}
.applicant-close-modal:hover {
color: var(--gray-900);
}
.applicant-status-ribbon {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 8px 15px;
background-color: var(--primary-blue);
color: var(--white);
font-weight: 600;
border-radius: 8px 8px 0 0;
}
.modal-applicant-container {
display: flex;
padding: 50px 20px 20px;
}
.modal-applicant-left {
flex: 0 0 auto;
margin-right: 20px;
}
.modal-applicant-image {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--primary-blue);
}
.modal-applicant-initials {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: var(--primary-blue);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 48px;
border: 3px solid var(--primary-blue);
}
.modal-applicant-right {
flex: 1;
}
.modal-applicant-name {
margin-top: 0;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.modal-applicant-details {
margin-top: 15px;
}
.detail-row {
display: flex;
margin-bottom: 10px;
}
.detail-label {
flex: 0 0 140px;
font-weight: 600;
color: var(--text-secondary);
}
.detail-value {
flex: 1;
color: var(--text-primary);
}
.recruiter-info {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
}
.recruiter-avatar {
display: flex;
align-items: center;
}
.recruiter-image {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-right: 10px;
}
.recruiter-initials {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--gray-300);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.recruiter-tooltip {
font-weight: 500;
color: var(--text-primary);
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -0,0 +1,850 @@
/** @odoo-module **/
function initApplicantsPage() {
console.log("Applicants Page Loaded");
const applicantDetailArea = document.getElementById("applicants-detail");
const container = document.querySelector('.ats-list-container');
const toggleBtn = document.getElementById("applicants-list-sidebar-toggle-btn");
const sidebar = document.getElementById("applicants-list-panel");
// Fix: Use correct class for applicant items - using both classes
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
item.addEventListener("click", function() {
console.log("Applicant item clicked"); // Add this for debugging
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
this.classList.add("selected");
applicantDetailArea.style.display = 'block';
container.classList.add('ats-selected');
sidebar.classList.remove('collapsed');
toggleBtn.style.display = 'flex';
const applicantId = this.dataset.id;
console.log("Applicant ID:", applicantId); // Add this for debugging
// Show loading state
if (applicantDetailArea) {
applicantDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading applicant details...</p></div>';
}
fetch(`/myATS/applicant/detail/${applicantId}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.text();
})
.then(html => {
console.log("Response received"); // Add this for debugging
if (applicantDetailArea) {
applicantDetailArea.innerHTML = html;
initApplicantDetailEdit(); // Initialize edit functionality
// Add close button functionality
const closeBtn = applicantDetailArea.querySelector('.close-detail');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
applicantDetailArea.style.display = 'none';
container.classList.remove('ats-selected');
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
});
}
}
})
.catch(error => {
console.error('Error loading applicant details:', error);
if (applicantDetailArea) {
applicantDetailArea.innerHTML = '<div class="alert alert-danger">Error loading applicant details. Please try again.</div>';
}
});
});
});
// Search functionality - use correct ID
const search = document.getElementById("applicants-search");
if (search) {
search.addEventListener("input", function() {
const query = this.value.toLowerCase();
let visibleCount = 0;
// Also fix this selector to use both classes
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
const match = item.textContent.toLowerCase().includes(query);
item.style.display = match ? "" : "none";
if (match) visibleCount++;
});
const countElement = document.getElementById("active-records-count");
if (countElement) {
countElement.textContent = visibleCount;
}
});
}
// Sidebar Toggle
if (toggleBtn && sidebar) {
toggleBtn.addEventListener("click", function(e) {
e.stopPropagation();
sidebar.classList.toggle("collapsed");
});
}
// Applicant Modal Handling
const modal = document.querySelector('.applicant-detail-modal');
if (modal) {
const closeModal = modal.querySelector('.applicant-close-modal');
// Event delegation for image clicks - use correct classes
document.addEventListener('click', function(e) {
const img = e.target.closest('.ats-item-image, .ats-item-initials');
if (!img) return;
const applicantItem = img.closest('.ats-item.applicants-item');
if (!applicantItem) return;
e.preventDefault();
e.stopImmediatePropagation();
// Get applicant data with null checks
const applicantId = applicantItem.dataset.id;
const nameElement = applicantItem.querySelector('.ats-title');
const jobElement = applicantItem.querySelector('#job_request');
const stageElement = applicantItem.querySelector('#applicant_stage');
const createDate = applicantItem.querySelector('#create_date');
const applicantEmail = applicantItem.querySelector('#applicant_email');
const applicantPhone = applicantItem.querySelector('#applicant_phone');
const alternatePhone = applicantItem.querySelector('#alternate_phone');
const recruiterName = applicantItem.querySelector('#recruiter_name');
const recruiterImage = applicantItem.querySelector('#recruiter_image');
const recruiterUserId = applicantItem.querySelector('#recruiter_id');
const name = nameElement ? nameElement.textContent : 'N/A';
const job = jobElement ? jobElement.textContent : 'N/A';
const stage = stageElement ? stageElement.textContent : 'N/A';
const date = createDate ? createDate.textContent : 'N/A';
const email = applicantEmail ? applicantEmail.textContent : 'N/A';
const phone = applicantPhone ? applicantPhone.textContent : 'N/A';
const altPhone = alternatePhone ? alternatePhone.textContent : 'N/A';
const recruiter = recruiterName ? recruiterName.textContent : 'N/A';
const recruiterId = recruiterUserId ? recruiterUserId.textContent: null;
const photoSrc = img.classList.contains('ats-item-image') ? img.src : null;
const initials = img.classList.contains('ats-item-initials') ? img.textContent : null;
// Populate modal with applicant data
modal.querySelector('.modal-applicant-name').textContent = name;
modal.querySelector('.modal-applicant-job').textContent = job;
modal.querySelector('.modal-applicant-stage').textContent = stage;
modal.querySelector('.modal-applicant-date').textContent = date;
modal.querySelector('.modal-applicant-email').textContent = email;
modal.querySelector('.modal-applicant-phone').textContent = phone;
modal.querySelector('.modal-applicant-altphone').textContent = altPhone;
// Handle applicant image
if (photoSrc) {
const modalImg = modal.querySelector('.modal-applicant-image');
modalImg.src = photoSrc;
modalImg.style.display = 'block';
modal.querySelector('.modal-applicant-initials').style.display = 'none';
} else {
modal.querySelector('.modal-applicant-initials').textContent = initials;
modal.querySelector('.modal-applicant-initials').style.display = 'flex';
modal.querySelector('.modal-applicant-image').style.display = 'none';
}
// Handle recruiter info
const recruiterImageEl = modal.querySelector('.recruiter-image');
const recruiterInitialsEl = modal.querySelector('.recruiter-initials');
const recruiterTooltip = modal.querySelector('.recruiter-tooltip');
if (recruiterImage && recruiterId) {
const recruiterSrc = `/web/image/res.users/${recruiterId}/image_128`;
recruiterImageEl.src = recruiterSrc;
recruiterImageEl.style.display = 'block';
recruiterInitialsEl.style.display = 'none';
recruiterSrc.onerror = function () {
recruiterImageEl.style.display = 'none';
};
} else if (recruiterName) {
recruiterInitialsEl.textContent = recruiterName.textContent.charAt(0).toUpperCase();
recruiterInitialsEl.style.display = 'flex';
recruiterImageEl.style.display = 'none';
}
recruiterTooltip.textContent = recruiter;
// Set status ribbon class based on stage - use correct selector
const statusRibbon = modal.querySelector('.applicant-status-ribbon');
if (statusRibbon) {
// Remove all existing status classes
statusRibbon.classList.remove('new', 'interview', 'hired', 'rejected');
// Add appropriate class based on stage
if (stage.toLowerCase().includes('interview')) {
statusRibbon.classList.add('interview');
} else if (stage.toLowerCase().includes('hired')) {
statusRibbon.classList.add('hired');
} else if (stage.toLowerCase().includes('reject')) {
statusRibbon.classList.add('rejected');
} else {
statusRibbon.classList.add('new');
}
}
// Show modal
modal.style.display = 'flex';
setTimeout(() => {
modal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
});
// Close modal handlers
closeModal.addEventListener('click', function() {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.classList.remove('show');
document.body.style.overflow = '';
}
});
// Close with ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.classList.contains('show')) {
modal.classList.remove('show');
document.body.style.overflow = '';
}
});
}
const createApplication = document.getElementById('add-application-create-btn');
const applicantModal = document.getElementById('application-creation-modal');
const closeModal = document.querySelectorAll('.application-creation-close, .btn-cancel');
if (createApplication) {
createApplication.addEventListener('click', function(e) {
e.preventDefault();
applicantModal.style.display = 'flex';
setTimeout(() => {
applicantModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
setTimeout(() => {
initSelect2();
initResumeUploadHandlers();
}, 100);
setTimeout(createApplicationForm, 100);
});
}
if (closeModal) {
closeModal.forEach(btn => {
btn.addEventListener('click', function() {
applicantModal.classList.remove('show');
setTimeout(() => {
applicantModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
});
}
// Close modal when clicking outside of it
applicantModal.addEventListener('click', function(e) {
if (e.target === applicantModal) {
applicantModal.classList.remove('show');
setTimeout(() => {
applicantModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
}
});
// File Upload Handling
const resumeUpload = document.getElementById('resume-upload');
const resumeDropzone = document.getElementById('resume-dropzone');
const resumePreview = document.getElementById('resume-preview');
const resumePlaceholder = resumePreview.querySelector('.resume-preview-placeholder');
const resumeIframe = document.getElementById('resume-iframe');
const resumeImage = document.getElementById('resume-image');
const unsupportedFormat = document.getElementById('unsupported-format');
const downloadResume = document.getElementById('download-resume');
const attachmentsList = document.querySelector('.attachments-list');
const addAttachmentBtn = document.querySelector('.add-attachment');
function initResumeUploadHandlers() {
// Create remove button
const removeResumeBtn = document.createElement('button');
removeResumeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove Resume';
removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
removeResumeBtn.style.display = 'none';
resumePreview.appendChild(removeResumeBtn);
// Handle remove resume
removeResumeBtn.addEventListener('click', function() {
resetResumePreview();
});
function resetResumePreview() {
// Clear file input
resumeUpload.value = '';
currentResumeFile = null;
// Reset preview
resumePlaceholder.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
removeResumeBtn.style.display = 'none';
// Reset iframe/src to prevent memory leaks
if (resumeIframe.src) {
URL.revokeObjectURL(resumeIframe.src);
resumeIframe.src = '';
}
if (resumeImage.src) {
URL.revokeObjectURL(resumeImage.src);
resumeImage.src = '';
}
if (downloadResume.href) {
URL.revokeObjectURL(downloadResume.href);
downloadResume.href = '#';
}
}
// Handle click on dropzone
resumeDropzone.addEventListener('click', function(e) {
if (e.target === this || e.target.classList.contains('upload-icon') ||
e.target.tagName === 'H5' || e.target.tagName === 'P') {
resumeUpload.click();
}
});
// Handle drag and drop
resumeDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.add('dragover');
this.style.borderColor = '#3498db';
this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
});
resumeDropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
});
resumeDropzone.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
handleResumeFile(file);
}
});
// Handle file selection from the regular input
resumeUpload.addEventListener('change', function(e) {
if (this.files.length) {
handleResumeFile(this.files[0]);
}
});
function handleResumeFile(file) {
const validTypes = [
'application/pdf',
'application/msword',
'application/wps-office.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'text/plain'
];
if (!validTypes.includes(file.type)) {
alert('Please upload a valid file type (PDF, Word, Image, or Text)');
return;
}
currentResumeFile = file;
// Hide placeholder
resumePlaceholder.style.display = 'none';
// Set up download link
const fileURL = URL.createObjectURL(file);
downloadResume.href = fileURL;
downloadResume.download = file.name;
removeResumeBtn.style.display = 'block';
// Check file type and show appropriate preview
if (file.type === 'application/pdf') {
// PDF preview
resumeIframe.src = fileURL;
resumeIframe.style.display = 'block';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else if (file.type.match('image.*')) {
// Image preview
resumeImage.src = fileURL;
resumeImage.style.display = 'block';
resumeIframe.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else {
// Unsupported format for preview
unsupportedFormat.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
}
// Update the actual resume-upload input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
resumeUpload.files = dataTransfer.files;
}
}
if (addAttachmentBtn) {
addAttachmentBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = '.pdf,.doc,.docx,.jpg,.png';
input.onchange = (e) => {
if (e.target.files.length) {
handleFiles(e.target.files, true);
}
};
input.click();
});
}
function createApplicationForm() {
const anniversaryField = document.getElementById('marital-anniversary-field');
const maritalStatus = document.getElementById('application-marital');
maritalStatus.addEventListener('change', () => {
if (maritalStatus.value !== 'married') {
document.getElementById('marital-anniversary-field').style.display = 'none';
document.getElementById('application-anniversary').setAttribute('disabled', 'disabled');
} else {
document.getElementById('marital-anniversary-field').style.display = 'block';
document.getElementById('application-anniversary').removeAttribute('disabled');
}
});
}
function initSelect2() {
// Check if Select2 is already initialized
const applicantSkills = document.getElementById('application-skills');
if (applicantSkills) {
$(applicantSkills).select2({
placeholder: 'Select skills',
allowClear: true,
dropdownParent: $('.application-creation-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const applicantPosition = document.getElementById('application-position');
if (applicantPosition) {
$(applicantPosition).select2({
placeholder: 'Select Job',
allowClear: false,
dropdownParent: $('.application-creation-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const applicantCandidate = document.getElementById('application-candidate');
if (applicantCandidate) {
$(applicantCandidate).select2({
placeholder: 'Select Candidate',
allowClear: true,
dropdownParent: $('.application-creation-modal'),
width: '100%',
templateResult: formatCandidate,
templateSelection: formatCandidateSelection,
escapeMarkup: function(m) { return m; }
}).on('change', function() {
const selectedOption = $(this).find('option:selected');
if (selectedOption.val()) {
// Populate fields from candidate data
$('#application-email').val(selectedOption.data('email') || '');
$('#application-phone').val(selectedOption.data('phone') || '');
$('#application-alt-phone').val(selectedOption.data('altPhone') || '');
$('#application-linkedin').val(selectedOption.data('linkedin') || '');
// For skills
let skillIds = selectedOption.data('skillIds');
// Convert to array if it's a string
if (typeof skillIds === 'string') {
try {
skillIds = JSON.parse(skillIds);
} catch (e) {
skillIds = [];
}
}
// Ensure skillIds is an array
skillIds = Array.isArray(skillIds) ? skillIds : [];
// Set the values in Select2 and trigger change
$('#application-skills').val(skillIds).trigger('change');
} else {
$('#application-email').val('');
$('#application-phone').val('');
$('#application-alt-phone').val('');
$('#application-linkedin').val('');
// For skills
let skillIds = [];
// Ensure skillIds is an array
skillIds = Array.isArray(skillIds) ? skillIds : [];
$('#application-skills').val(skillIds).trigger('change');
}
});
}
}
function formatCandidate(candidate) {
if (!candidate.id) {
return candidate.text;
}
var imageUrl = $(candidate.element).data('image') || '/web/static/img/placeholder.png';
var $candidate = $(
'<span style="display: flex; align-items: center;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + candidate.text + '</span>' +
'</span>'
);
return $candidate;
}
function formatCandidateSelection(candidate) {
if (!candidate.id) {
return candidate.text;
}
var imageUrl = $(candidate.element).data('image') || '/web/static/img/placeholder.png';
var $candidate = $(
'<span style="display: flex; align-items: center; width: 100%;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1;">' + candidate.text + '</span>' +
'</span>'
);
return $candidate;
}
function handleFiles(files, isAdditional = false) {
Array.from(files).forEach(file => {
if (!file.type.match('application/pdf|application/msword|application/vnd.openxmlformats-officedocument.wordprocessingml.document|image.*')) {
alert('Only PDF, DOC, DOCX, JPG, and PNG files are allowed');
return;
}
if (file.size > 10 * 1024 * 1024) { // 10MB limit
alert('File size must be less than 10MB');
return;
}
if (isAdditional) {
addAttachmentToList(file);
} else {
updateResumeUpload(file);
}
});
}
function updateResumeUpload(file) {
// You can preview or process the resume file here
const dropzone = document.getElementById('resume-dropzone');
dropzone.innerHTML = `
<i class="fas fa-check-circle upload-icon" style="color:var(--success)"></i>
<h5>${file.name}</h5>
<p>${(file.size / 1024 / 1024).toFixed(2)} MB</p>
<button type="button" class="btn-remove remove-resume">
<i class="fas fa-times"></i> Remove
</button>
<input type="file" id="resume-upload" name="resume" class="file-input" style="display:none"/>
`;
// Re-attach event listeners
document.querySelector('.remove-resume').addEventListener('click', () => {
resetResumeUpload();
});
}
function resetResumeUpload() {
const dropzone = document.getElementById('resume-dropzone');
dropzone.innerHTML = `
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<h5>Upload Resume</h5>
<p>Drag & drop your resume here or click to browse</p>
<input type="file" id="resume-upload" name="resume" accept=".pdf,.doc,.docx" class="file-input"/>
`;
// Re-attach event listeners
setupFileUpload();
}
function addAttachmentToList(file) {
const attachmentItem = document.createElement('div');
attachmentItem.className = 'attachment-item';
attachmentItem.innerHTML = `
<div class="attachment-info">
<div class="attachment-header">
<span class="attachment-title">${file.name}</span>
<span class="attachment-size">${(file.size / 1024 / 1024).toFixed(2)} MB</span>
</div>
<div class="attachment-actions">
<button type="button" class="btn-remove remove-attachment">
<i class="fas fa-times"></i> Remove
</button>
</div>
</div>
`;
attachmentsList.appendChild(attachmentItem);
attachmentItem.querySelector('.remove-attachment').addEventListener('click', () => {
attachmentItem.remove();
});
}
function setupFileUpload() {
// Re-initialize event listeners if needed
const resumeDropzone = document.getElementById('resume-dropzone');
const resumeUpload = document.getElementById('resume-upload');
if (resumeDropzone && resumeUpload) {
resumeDropzone.addEventListener('click', () => {
resumeUpload.click();
});
}
}
const uploadBtn = document.getElementById('upload-resume')
if (uploadBtn) {
uploadBtn.addEventListener('click', function (e) {
e.preventDefault();
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.doc,.docx,.txt';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// Show loading state
const button = document.getElementById('upload-resume');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing Resume...';
button.disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'applicant');
const response = await fetch('/resume/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
populateApplicationForm(result);
} catch (error) {
console.error('Error parsing Resume:', error);
showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
} finally {
button.innerHTML = originalText;
button.disabled = false;
}
};
fileInput.click();
});
}
function populateApplicationForm(resumeData) {
// Add this CSS class definition dynamically
const style = document.createElement('style');
style.textContent = `
.populated-field {
background-color: #f5f5f5 !important;
transition: background-color 0.3s ease;
}
`;
document.head.appendChild(style);
// Helper function to set value with visual feedback
function setValueWithFeedback(element, value) {
if (element && value) {
element.value = value;
element.classList.add('populated-field');
return true;
}
return false;
}
// Helper function for select elements
function setSelectValueWithFeedback(selectElement, value, compareAsText = false) {
if (!selectElement || !value) return false;
for (let i = 0; i < selectElement.options.length; i++) {
const option = selectElement.options[i];
const match = compareAsText
? option.text.toLowerCase().includes(value.toLowerCase())
: option.value.toLowerCase() === value.toLowerCase();
if (match) {
selectElement.value = option.value;
selectElement.classList.add('populated-field');
return true;
}
}
return false;
}
// Section 1: Basic Information
if (resumeData.personal_info) {
const personal = resumeData.personal_info;
if (resumeData.candidate_id) {
let candidateId = resumeData.candidate_id;
$('#application-candidate').val(candidateId).trigger('change')
.addClass('populated-field');
} else {
setValueWithFeedback(document.getElementById('application-fullname'), personal.name);
setValueWithFeedback(document.getElementById('application-email'), personal.email);
setValueWithFeedback(document.getElementById('application-phone'), personal.phone);
setValueWithFeedback(document.getElementById('application-linkedin'), personal.linkedin);
// Skills
if (resumeData.skills?.length) {
const skillValues = resumeData.skills.map(skill => skill.id);
$('#application-skills').val(skillValues).trigger('change')
.addClass('populated-field');
}
}
setSelectValueWithFeedback(
document.getElementById('application-gender'),
personal.gender
);
setValueWithFeedback(document.getElementById('application-dob'), personal.dob);
}
// Professional Information
if (resumeData.professional_info) {
const prof = resumeData.professional_info;
setValueWithFeedback(document.getElementById('application-current-org'), prof.current_company);
setValueWithFeedback(
document.getElementById('application-current-location'),
prof.current_location?.city
);
setValueWithFeedback(document.getElementById('application-notice-period'), prof.notice_period);
if (prof.notice_period) {
setSelectValueWithFeedback(
document.getElementById('application-notice-negotiable'),
'yes'
);
}
setSelectValueWithFeedback(
document.getElementById('application-holding-offer'),
prof.holding_offer ? 'yes' : 'no'
);
setValueWithFeedback(document.getElementById('application-total-exp'), prof.total_experience);
}
// Salary Information
setValueWithFeedback(document.getElementById('application-current-ctc'), resumeData.current_ctc);
setValueWithFeedback(document.getElementById('application-expected-salary'), resumeData.expected_salary);
// Address Information
if (resumeData.contact_info?.current_address) {
const addr = resumeData.contact_info.current_address;
setValueWithFeedback(document.getElementById('application-current-street'), addr.street);
setValueWithFeedback(document.getElementById('application-current-city'), addr.city);
setSelectValueWithFeedback(
document.getElementById('application-current-state'),
addr.state,
true // Compare as text
);
setSelectValueWithFeedback(
document.getElementById('application-current-country'),
addr.country,
true // Compare as text
);
setValueWithFeedback(document.getElementById('application-current-zip'), addr.zip);
}
// Show notification
showNotification('Resume uploaded and fields populated successfully!', 'success');
}
// Helper function to show notifications
function showNotification(message, type = 'info') {
// Check if notification container exists, create if not
let notificationContainer = document.getElementById('notification-container');
if (!notificationContainer) {
notificationContainer = document.createElement('div');
notificationContainer.id = 'notification-container';
notificationContainer.style.position = 'fixed';
notificationContainer.style.top = '20px';
notificationContainer.style.right = '20px';
notificationContainer.style.zIndex = '9999';
document.body.appendChild(notificationContainer);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add to container
notificationContainer.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 5000);
}
}
function scrollToTarget(targetElement, offset = 100) {
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - offset;
// First try scrolling the container
const container = document.getElementById('ats-details-container');
if (container && container.scrollHeight > container.clientHeight) {
const containerTop = container.getBoundingClientRect().top;
const scrollPosition = targetPosition - containerTop - offset;
container.scrollTo({
top: scrollPosition,
behavior: 'smooth'
});
} else {
// Fallback to window scrolling
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}
}
function initApplicantDetailEdit() {
// Recruiter photo toggle
const recruiterTrigger = document.getElementById('recruiter-photo-trigger');
const recruiterInfo = document.getElementById('recruiter-info');
if (recruiterTrigger && recruiterInfo) {
recruiterTrigger.addEventListener('click', function() {
recruiterInfo.classList.toggle('show');
});
}
// Improved smooth scroll navigation
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
scrollToTarget(targetElement);
// Highlight effect
targetElement.style.boxShadow = '0 0 0 3px rgba(13, 110, 253, 0.5)';
targetElement.style.transition = 'box-shadow 0.3s ease';
setTimeout(() => {
targetElement.style.boxShadow = 'none';
}, 2000);
}
});
});
}
// Initialize the page when DOM is ready
document.addEventListener('DOMContentLoaded', initApplicantsPage);

View File

@ -0,0 +1,46 @@
/** @odoo-module */
document.addEventListener("DOMContentLoaded", function () {
console.log("ATS JS Loaded");
const contentArea = document.getElementById("main-content");
const jobDetailArea = document.getElementById("job-detail");
const toggleBtn = document.getElementById("sidebar-toggle-btn");
const sidebar = document.getElementById("sidebar");
// ✅ Sidebar Toggle
if (toggleBtn && sidebar) {
toggleBtn.addEventListener("click", function () {
sidebar.classList.toggle("collapsed");
sidebar.classList.toggle("expanded");
});
}
document.querySelectorAll('.menu-list a[data-page]').forEach(link => {
link.addEventListener('click', async function (e) {
e.preventDefault();
const page = this.dataset.page;
try {
const res = await fetch(`/myATS/page/${page}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
});
const html = await res.text();
contentArea.innerHTML = html;
if (jobDetailArea) jobDetailArea.innerHTML = "";
// 🔽 Dynamically import JS module for the page
if (page === "job_requests") {
initJobListPage();
} else if (page === "applicants") {
initApplicantsPage();
} else if (page === "candidates") {
initCandidatesPage();
}
} catch (err) {
contentArea.innerHTML = `<p style="color:red;">Error loading page: ${err}</p>`;
}
});
});
});

View File

@ -0,0 +1,608 @@
/** @odoo-module **/
function initCandidatesPage() {
console.log("candidates Page Loaded");
const candidateDetailArea = document.getElementById("candidates-detail");
const container = document.querySelector('.ats-list-container'); // Added this line
const toggleBtn = document.getElementById("candidates-list-sidebar-toggle-btn"); // Added this line
const sidebar = document.getElementById("candidates-list-panel"); // Added this line
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
item.addEventListener("click", function() {
document.querySelectorAll(".candidates-item.selected").forEach(el => el.classList.remove("selected"));
this.classList.add("selected");
// Show the detail panel and add necessary classes
candidateDetailArea.style.display = 'block'; // Added this line
container.classList.add('ats-selected'); // Added this line
sidebar.classList.remove('collapsed'); // Added this line
toggleBtn.style.display = 'flex'; // Added this line
const candidateId = this.dataset.id;
console.log("Candidate ID:", candidateId); // Added for debugging
// Show loading state
if (candidateDetailArea) {
candidateDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading candidate details...</p></div>';
}
fetch(`/myATS/candidate/detail/${candidateId}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.text();
})
.then(html => {
console.log("Response received"); // Added for debugging
if (candidateDetailArea) {
candidateDetailArea.innerHTML = html;
// Add close button functionality
const closeBtn = candidateDetailArea.querySelector('.close-detail');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
candidateDetailArea.style.display = 'none';
container.classList.remove('ats-selected');
document.querySelectorAll(".ats-item.candidates-item.selected").forEach(el => el.classList.remove("selected"));
});
}
}
})
.catch(error => {
console.error('Error loading candidate details:', error);
if (candidateDetailArea) {
candidateDetailArea.innerHTML = '<div class="alert alert-danger">Error loading candidate details. Please try again.</div>';
}
});
});
});
// Search functionality
const search = document.getElementById("candidates-search");
if (search) {
search.addEventListener("input", function() {
const query = this.value.toLowerCase();
let visibleCount = 0;
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
const match = item.textContent.toLowerCase().includes(query);
item.style.display = match ? "" : "none";
if (match) visibleCount++;
});
const countElement = document.getElementById("active-records-count");
if (countElement) {
countElement.textContent = visibleCount;
}
});
}
// Sidebar Toggle
if (toggleBtn && sidebar) { // Added this check
toggleBtn.addEventListener("click", function(e) {
e.stopPropagation();
sidebar.classList.toggle("collapsed");
});
}
// Rest of your code remains the same...
const createCandidate = document.getElementById('add-candidate-create-btn');
const candidateModal = document.getElementById('candidate-form-modal');
const form = document.getElementById('candidate-form');
const closeModal = document.querySelectorAll('.candidate-form-close, .btn-cancel');
const avatarUpload = document.getElementById('avatar-upload');
const candidateImage = document.getElementById('candidate-image');
const avatarUploadIcon = document.querySelector('.avatar-upload i');
if (avatarUpload && candidateImage) {
// Make the entire avatar clickable
candidateImage.parentElement.style.cursor = 'pointer';
// Handle click on avatar
candidateImage.parentElement.addEventListener('click', function(e) {
if (e.target !== avatarUpload && e.target !== avatarUploadIcon) {
avatarUpload.click();
}
});
// Handle file selection
avatarUpload.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.match('image.*')) {
const reader = new FileReader();
reader.onload = function(e) {
candidateImage.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
}
// Resume Upload Elements
const resumeUpload = document.getElementById('resume-upload');
const resumeDropzone = document.getElementById('resume-dropzone');
const resumePreview = document.getElementById('resume-preview');
const resumePlaceholder = resumePreview.querySelector('.resume-preview-placeholder');
const resumeIframe = document.getElementById('resume-iframe');
const resumeImage = document.getElementById('resume-image');
const unsupportedFormat = document.getElementById('unsupported-format');
const downloadResume = document.getElementById('download-resume');
const uploadResumeBtn = document.getElementById('upload-applicant-resume');
let currentResumeFile = null;
const saveBtn = document.getElementById('save-candidate');
if (saveBtn) {
saveBtn.addEventListener('click', function (e) {
e.preventDefault();
createNewCandidate(form, candidateModal);
});
}
if (createCandidate) {
createCandidate.addEventListener('click', function(e) {
e.preventDefault();
candidateModal.style.display = 'flex';
setTimeout(() => {
candidateModal.classList.add('show');
}, 10);
document.body.style.overflow = 'hidden';
setTimeout(() => {
initSelect2();
initResumeUploadHandlers();
}, 100);
});
}
if (closeModal) {
closeModal.forEach(btn => {
btn.addEventListener('click', function() {
candidateModal.classList.remove('show');
setTimeout(() => {
candidateModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
});
});
}
candidateModal.addEventListener('click', function(e) {
if (e.target === candidateModal) {
candidateModal.classList.remove('show');
setTimeout(() => {
candidateModal.style.display = 'none';
}, 300);
document.body.style.overflow = '';
}
});
function formatUserOption(user) {
if (!user.id) return user.text;
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
var $container = $(
'<span style="display: flex; align-items: center;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + user.text + '</span>' +
'</span>'
);
return $container;
}
function formatUserSelection(user) {
if (!user.id) return user.text;
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
var $container = $(
'<span style="display: flex; align-items: center; width: 100%;">' +
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1;">' + user.text + '</span>' +
'</span>'
);
return $container;
}
function initSelect2() {
const candidateSkills = document.getElementById('candidate-skills');
if (candidateSkills) {
$(candidateSkills).select2({
placeholder: 'Select skills',
allowClear: true,
dropdownParent: $('.candidate-form-modal'),
width: '100%',
escapeMarkup: function(m) { return m; }
});
}
const managerSelect = document.getElementById('manager');
if (managerSelect) {
$(managerSelect).select2({
placeholder: 'Select Manager',
allowClear: true,
templateResult: formatUserOption,
templateSelection: formatUserSelection,
escapeMarkup: function(m) { return m; }
});
}
}
function initResumeUploadHandlers() {
// Create remove button
const removeResumeBtn = document.createElement('button');
removeResumeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove Resume';
removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
removeResumeBtn.style.display = 'none';
resumePreview.appendChild(removeResumeBtn);
// Handle remove resume
removeResumeBtn.addEventListener('click', function() {
resetResumePreview();
});
function resetResumePreview() {
// Clear file input
resumeUpload.value = '';
currentResumeFile = null;
// Reset preview
resumePlaceholder.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
removeResumeBtn.style.display = 'none';
// Reset iframe/src to prevent memory leaks
if (resumeIframe.src) {
URL.revokeObjectURL(resumeIframe.src);
resumeIframe.src = '';
}
if (resumeImage.src) {
URL.revokeObjectURL(resumeImage.src);
resumeImage.src = '';
}
if (downloadResume.href) {
URL.revokeObjectURL(downloadResume.href);
downloadResume.href = '#';
}
}
// Unified upload handler for both preview and parsing
uploadResumeBtn.addEventListener('click', function(e) {
e.preventDefault();
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
// First handle the preview
handleResumeFile(file);
// Then try to parse the resume if it's a parseable type
if (file.type.match(/pdf|msword|openxmlformats|text/)) {
// Show loading state
const button = uploadResumeBtn;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing Resume...';
button.disabled = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('type', 'candidate');
const response = await fetch('/resume/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
populateCandidateForm(result);
} catch (error) {
console.error('Error parsing Resume:', error);
showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
} finally {
button.innerHTML = '<i class="fas fa-upload"></i> Upload Resume';
button.disabled = false;
}
}
};
fileInput.click();
});
// Handle click on dropzone
resumeDropzone.addEventListener('click', function(e) {
if (e.target === this || e.target.classList.contains('upload-icon') ||
e.target.tagName === 'H5' || e.target.tagName === 'P') {
resumeUpload.click();
}
});
// Handle drag and drop
resumeDropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.add('dragover');
this.style.borderColor = '#3498db';
this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
});
resumeDropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
});
resumeDropzone.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('dragover');
this.style.borderColor = '';
this.style.backgroundColor = '';
if (e.dataTransfer.files.length) {
const file = e.dataTransfer.files[0];
handleResumeFile(file);
}
});
// Handle file selection from the regular input
resumeUpload.addEventListener('change', function(e) {
if (this.files.length) {
handleResumeFile(this.files[0]);
}
});
function handleResumeFile(file) {
const validTypes = [
'application/pdf',
'application/msword',
'application/wps-office.docx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'text/plain'
];
if (!validTypes.includes(file.type)) {
alert('Please upload a valid file type (PDF, Word, Image, or Text)');
return;
}
currentResumeFile = file;
// Hide placeholder
resumePlaceholder.style.display = 'none';
// Set up download link
const fileURL = URL.createObjectURL(file);
downloadResume.href = fileURL;
downloadResume.download = file.name;
removeResumeBtn.style.display = 'block';
// Check file type and show appropriate preview
if (file.type === 'application/pdf') {
// PDF preview
resumeIframe.src = fileURL;
resumeIframe.style.display = 'block';
resumeImage.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else if (file.type.match('image.*')) {
// Image preview
resumeImage.src = fileURL;
resumeImage.style.display = 'block';
resumeIframe.style.display = 'none';
unsupportedFormat.style.display = 'none';
} else {
// Unsupported format for preview
unsupportedFormat.style.display = 'flex';
resumeIframe.style.display = 'none';
resumeImage.style.display = 'none';
}
// Update the actual resume-upload input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
resumeUpload.files = dataTransfer.files;
}
}
function populateCandidateForm(resumeData) {
// Add CSS for visual feedback
const style = document.createElement('style');
style.textContent = `
.populated-field {
background-color: #f5f5f5 !important;
transition: background-color 0.3s ease;
}
.select2-populated .select2-selection--multiple {
background-color: #f5f5f5 !important;
}
.select2-populated .select2-selection__choice {
background-color: #e8f5e9 !important;
border-color: #c8e6c9 !important;
}
`;
document.head.appendChild(style);
// Helper function to set value with visual feedback
function setValueWithFeedback(elementId, value) {
const element = document.getElementById(elementId);
if (element && value) {
element.value = value;
element.classList.add('populated-field');
// Special handling for Select2 if this element uses it
if ($(element).hasClass('select2-hidden-accessible')) {
$(element).next('.select2-container')
.find('.select2-selection')
.addClass('populated-field');
}
return true;
}
return false;
}
// Section 1: Basic Information
if (resumeData.personal_info) {
const personal = resumeData.personal_info;
// Set values with visual feedback
setValueWithFeedback('partner-name', personal.name);
setValueWithFeedback('email', personal.email);
setValueWithFeedback('phone', personal.phone);
setValueWithFeedback('linkedin', personal.linkedin);
// If any of these fields are Select2 elements, ensure they get styled
['partner-name', 'email', 'phone', 'linkedin'].forEach(id => {
const el = document.getElementById(id);
if (el && el.value && $(el).hasClass('select2-hidden-accessible')) {
$(el).next('.select2-container')
.find('.select2-selection')
.addClass('populated-field');
}
});
}
// Skills - Handle Select2 with background color change
if (resumeData.skills?.length) {
const skillValues = resumeData.skills.map(skill => skill.id);
$('#candidate-skills')
.val(skillValues)
.trigger('change')
.addClass('populated-field');
// Add class to Select2 container for styling
$('#candidate-skills').next('.select2-container')
.find('.select2-selection--multiple')
.addClass('select2-populated');
}
// Show notification
showNotification('Resume uploaded and fields populated successfully!', 'success');
}
function showNotification(message, type) {
// Check if notification container exists, create if not
let notificationContainer = document.getElementById('notification-container');
if (!notificationContainer) {
notificationContainer = document.createElement('div');
notificationContainer.id = 'notification-container';
notificationContainer.style.position = 'fixed';
notificationContainer.style.top = '20px';
notificationContainer.style.right = '20px';
notificationContainer.style.zIndex = '9999';
document.body.appendChild(notificationContainer);
}
// Create notification element
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show`;
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
// Add to container
notificationContainer.appendChild(notification);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 5000);
}
}
function createNewCandidate(form, modal) {
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData();
// Basic Information
formData.append('sequence', document.getElementById('candidate-sequence').value);
formData.append('partner_name', document.getElementById('partner-name').value);
// Add image file if selected
const avatarUpload = document.getElementById('avatar-upload');
if (avatarUpload.files.length > 0) {
formData.append('image_1920', avatarUpload.files[0]);
}
// Contact Information
formData.append('email', document.getElementById('email').value);
formData.append('phone', document.getElementById('phone').value);
formData.append('mobile', document.getElementById('alternate-phone').value);
formData.append('linkedin_profile', document.getElementById('linkedin').value);
formData.append('type_id', document.getElementById('type').value);
formData.append('user_id', document.getElementById('manager').value);
formData.append('availability', document.getElementById('availability').value);
// Skills
const skillOptions = document.getElementById('candidate-skills').selectedOptions;
for (let i = 0; i < skillOptions.length; i++) {
formData.append('skill_ids', skillOptions[i].value);
}
// Add resume file if selected
const resumeUpload = document.getElementById('resume-upload');
if (resumeUpload.files.length > 0) {
formData.append('resume_file', resumeUpload.files[0]);
}
// Additional fields
formData.append('active', 'true');
formData.append('color', '0');
fetch('/myATS/candidate/create', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
})
.then(async (response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || response.statusText);
}
return response.json();
})
.then(data => {
if (data.success) {
modal.classList.remove('show');
document.body.style.overflow = '';
form.reset();
alert('Candidate created successfully!');
// Refresh the candidates list
fetch('/myATS/page/candidates', {
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newCandidate = doc.querySelector('.candidates-list-left');
const existingCandidatesList = document.querySelector('.candidates-list-left');
if (newCandidate && existingCandidatesList) {
existingCandidatesList.innerHTML = newCandidate.innerHTML;
initCandidatesPage();
}
});
} else {
throw new Error(data.error || 'Failed to save Candidate');
}
})
.catch(error => {
console.error('Error:', error);
alert("Error saving changes: " + error.message);
});
}
// Initialize the page when DOM is ready
document.addEventListener('DOMContentLoaded', initCandidatesPage);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,597 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="candidates_list_partial_page">
<div class="ats-list-container">
<!-- Job List and Details -->
<div class="ats-list-body">
<!-- Job List Panel -->
<div class="ats-list-left expanded" id="candidates-list-panel">
<t t-call="hr_recruitment_web_app.candidates_list"/>
<!-- Toggle Button -->
<button id="candidates-list-sidebar-toggle-btn" class="ats-list-toggle-btn"></button>
</div>
<!-- Job Detail Panel -->
<div id="candidates-detail" class="ats-detail">
<em>Select a candidate to view details.</em>
</div>
</div>
</div>
<t t-call="hr_recruitment_web_app.candidate_form_template"/>
</template>
<template id="candidates_list">
<div class="ats-list-container">
<!-- Search -->
<div class="ats-list-search">
<input type="text" id="candidates-search" placeholder="🔍 Search candidates..."/>
</div>
<!-- Action Buttons Header -->
<div class="ats-actions-header">
<button class="btn" type="button" id="activeRecords" style="left:0;">
Active Records (
<span id="active-records-count">
<t t-esc="len(candidates)"/>
</span>
)
</button>
<button class="btn add-create-btn" type="button" id="add-candidate-create-btn" style="right:0;">
<span class="plus-icon">+</span>
Add New candidate
</button>
</div>
<ul class="ats-list">
<t t-foreach="candidates" t-as="candidate">
<li class="ats-item candidates-item" t-att-data-id="candidate.id">
<div class="ats-item-content">
<div class="ats-info">
<div id="candidate_name" class="ats-title" style="padding-bottom:10px;">
<strong>
<t t-if="candidate.display_name">
<t t-esc="candidate.display_name"/>
</t>
</strong>
</div>
<div style="display:none;">
<span id="create_date" t-esc="candidate.create_date"/>
<span id="candidate_email" t-esc="candidate.email_from"/>
<span id="candidate_phone" t-esc="candidate.partner_phone"/>
<span id="alternate_phone" t-esc="candidate.alternate_phone"/>
<span id="recruiter_id" t-esc="candidate.user_id.id"/>
<span id="recruiter_name" t-esc="candidate.user_id.display_name"/>
<span id="recruiter_image" t-esc="candidate.user_id.image_128"/>
</div>
</div>
<div class="ats-avatar">
<t t-if="candidate.candidate_image">
<img t-att-src="image_data_uri(candidate.candidate_image)"
class="ats-item-image"
alt="candidate photo"/>
</t>
<t t-else="">
<div class="ats-item-initials">
<t t-if="candidate.display_name">
<t t-esc="candidate.display_name[0].upper()"/>
</t>
</div>
</t>
</div>
</div>
</li>
</t>
</ul>
</div>
</template>
<template id="candidates_detail_partial">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
<div class="ats-grid" id="ats-details-container">
<div class="ats-card span-3" style="grid-row: span 2;" id="candidate-overview">
<h4 class="section-title">
<i class="fa fa-list-ul me-2"></i>Quick Nav
</h4>
<div class="overview-nav">
<ul class="nav-list">
<li>
<a href="#candidate-basic-info" class="nav-link smooth-scroll"><i
class="fa fa-info-circle me-2"></i>Basic Info
</a>
</li>
<li>
<a href="#candidate-contact" class="nav-link smooth-scroll"><i
class="fa fa-address-book me-2"></i>Contact
</a>
</li>
<li>
<a href="#candidate-resume" class="nav-link smooth-scroll"><i
class="fa fa-file-pdf me-2"></i>Resume
</a>
</li>
<li>
<a href="#candidate-availability" class="nav-link smooth-scroll"><i
class="fa fa-calendar me-2"></i>Availability
</a>
</li>
<li>
<a href="#candidate-skills" class="nav-link smooth-scroll">
<i class="fa fa-star me-2"></i>
Skills
</a>
</li>
<li>
<a href="#candidate-employment" class="nav-link smooth-scroll"><i
class="fa fa-briefcase me-2"></i>Employment
</a>
</li>
<li>
<a href="#candidate-education" class="nav-link smooth-scroll"><i
class="fa fa-graduation-cap me-2"></i>Education
</a>
</li>
<li>
<a href="#candidate-family" class="nav-link smooth-scroll"><i
class="fa fa-users me-2"></i>Family
</a>
</li>
</ul>
</div>
</div>
<div class="ats-card span-9" style="grid-row: span 1;" id="candidate-profile">
<div class="ats-info">
<h1 class="ats-title">
<span t-esc="candidate.partner_name or 'Unnamed Candidate'"/>
</h1>
<div class="ats-meta">
<span class="me-3 meta-item">
<i class="fa fa-envelope me-1"></i>
<span t-esc="candidate.email_from or 'No email'"/>
</span>
<span class="me-3 meta-item">
<i class="fa fa-phone me-1"></i>
<span t-esc="candidate.partner_phone or 'No phone'"/>
</span>
<span class="me-3 meta-item">
<i class="fa fa-user-tie me-1"></i>
<span t-esc="candidate.type_id.display_name or 'No type specified'"/>
</span>
<span class="me-3 meta-item" t-if="candidate.employee_id">
<i class="fa fa-id-card me-1"></i>
<span t-esc="candidate.employee_id.name or 'No employee record'"/>
</span>
</div>
</div>
<div class="ats-avatar">
<div class="avatar-wrapper">
<img t-if="candidate.candidate_image"
t-att-src="image_data_uri(candidate.candidate_image)"
class="rounded-circle avatar-img animate__animated animate__fadeIn"
width="120" height="120" alt="Candidate Photo"/>
<div t-else=""
class="avatar-placeholder rounded-circle animate__animated animate__fadeIn">
<div class="applicant-img-placeholder">
<t t-if="candidate.display_name">
<t t-esc="candidate.display_name[0].upper()"/>
</t>
<t t-else="">
C
</t>
</div>
</div>
<div class="avatar-overlay">
<i class="fa fa-search-plus"></i>
</div>
</div>
</div>
</div>
<div class="ats-card span-3" style="grid-row: span 1;">
</div>
<div class="ats-card span-6" style="grid-row: span 1;" id="candidate-basic-info">
<h4 class="section-title">
<i class="fa fa-info-circle me-2"></i>Basic Information
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Type:</span>
<span class="detail-value" t-esc="candidate.type_id.display_name or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Evaluation:</span>
<span class="priority-stars">
<t t-set="priority_value" t-value="int(candidate.priority) if candidate.priority else 0"/>
<t t-foreach="range(5)" t-as="i">
<i t-att-class="'fa ' + ('fa-star' if priority_value > i else 'fa-star-o')"
t-att-style="'color: ' + ('var(--secondary-yellow)' if priority_value > i else 'var(--gray-300)') + '; ' +
('text-shadow: 0 0 8px var(--secondary-yellow); animation: pulse 1.5s infinite alternate;' if priority_value > i else '')"/>
</t>
</span>
</div>
</div>
</div>
<div class="ats-card span-6" style="grid-row: span 2;" id="candidate-contact">
<h4 class="section-title">
<i class="fa fa-address-book me-2"></i>Contact Information
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Email:</span>
<span class="detail-value" t-esc="candidate.email_from or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Phone:</span>
<span class="detail-value" t-esc="candidate.partner_phone or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Alternate Phone:</span>
<span class="detail-value" t-esc="candidate.alternate_phone or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">LinkedIn:</span>
<t t-if="candidate.linkedin_profile">
<a t-att-href="candidate.linkedin_profile" target="_blank"
class="detail-value social-link">
<i class="fa fa-linkedin-square me-1"></i>
<span>View Profile</span>
</a>
</t>
<t t-else="">
<span class="detail-value">N/A</span>
</t>
</div>
<div class="detail-row">
<span class="detail-label">Manager:</span>
<span class="detail-value" t-esc="candidate.user_id.display_name or 'Not assigned'"/>
</div>
</div>
</div>
<div class="ats-card span-3" id="candidate-resume">
<h4 class="section-title">
<i class="fa fa-file-pdf me-2"></i>Resume
</h4>
<div class="attachment-item">
<div class="attachment-info">
<div class="attachment-name attachment-header"
t-esc="candidate.resume_name or 'Resume'"/>
<div class="attachment-actions">
<!-- Preview Button -->
<a t-if="candidate.resume"
t-att-href="'/web/content/?model=hr.candidate&amp;id=' + str(candidate.id) + '&amp;field=resume&amp;download=false'"
class="btn btn-sm btn-outline-primary me-2 document-action-btn attachment-btn preview-btn"
target="_blank">
<i class="fa fa-eye me-1"></i>Preview
</a>
<!-- Download Button -->
<a t-if="candidate.resume"
t-att-href="'/web/content/?model=hr.candidate&amp;id=' + str(candidate.id) + '&amp;field=resume&amp;download=true'"
class="btn btn-sm btn-outline-success document-action-btn attachment-btn download-btn">
<i class="fa fa-download me-1"></i>Download
</a>
</div>
</div>
</div>
</div>
<div class="ats-card span-3" id="candidate-availability">
<h4 class="section-title">
<i class="fa fa-calendar me-2"></i>Availability
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value" t-esc="candidate.availability or 'Not specified'"/>
</div>
<div class="detail-row">
<span class="detail-label">Categories:</span>
<div class="skills-list">
<t t-foreach="candidate.categ_ids" t-as="category">
<span class="category-badge animate__animated animate__fadeInUp"
t-att-data-delay="category_index * 50"
t-esc="category.display_name"/>
</t>
<t t-if="not candidate.categ_ids">
<span class="text-muted">None specified</span>
</t>
</div>
</div>
</div>
</div>
<div class="ats-card span-12" id="candidate-skills">
<h4 class="section-title">
<i class="fa fa-star me-2"></i>Skills
<small class="text-muted float-end">
<span t-esc="len(candidate.candidate_skill_ids) or '0'"/>
skills recorded
</small>
</h4>
<div t-if="candidate.candidate_skill_ids and len(candidate.candidate_skill_ids) > 0"
class="skills-container">
<t t-foreach="candidate.candidate_skill_ids" t-as="skill">
<div class="skill-item animate__animated animate__fadeInUp"
t-att-data-delay="skill_index * 50">
<div class="skill-info">
<span class="skill-name" t-esc="skill.skill_id.display_name"/>
<span class="skill-type" t-esc="skill.skill_type_id.display_name"/>
</div>
<div class="skill-level">
<span class="skill-level-name"
t-esc="skill.skill_level_id.display_name"/>
<div class="progress">
<div class="progress-bar progress-bar-animated" role="progressbar"
t-att-style="'width: ' + str(skill.level_progress) + '%;'"
t-att-aria-valuenow="skill.level_progress"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
</div>
</t>
</div>
<t t-else="">
<div class="alert alert-info animate__animated animate__fadeIn">No skills recorded
</div>
</t>
</div>
<div class="ats-card span-12" id="candidate-employment">
<h4 class="section-title">
<i class="fa fa-briefcase me-2"></i>Employment History
</h4>
<div t-if="candidate.employer_history and len(candidate.employer_history) > 0"
class="experience-list">
<t t-foreach="candidate.employer_history" t-as="exp">
<div class="experience-item animate__animated animate__fadeInLeft"
t-att-data-delay="exp_index * 100">
<div class="exp-header">
<h5 t-esc="exp.designation or 'No designation'"/>
<span class="exp-company" t-esc="exp.company_name or 'No company'"/>
</div>
<div class="exp-duration">
<i class="fa fa-calendar me-1"></i>
<span t-esc="exp.date_of_joining or 'Start date not specified'"/>
<span>to</span>
<span t-esc="exp.last_working_day or 'Present'"/>
</div>
<div class="exp-ctc" t-if="exp.ctc">
<i class="fa fa-money-bill-wave me-1"></i>
<span t-esc="exp.ctc"/>
</div>
</div>
</t>
</div>
<t t-else="">
<div class="alert alert-info animate__animated animate__fadeIn">No employment history recorded
</div>
</t>
</div>
<div class="ats-card span-12" id="candidate-education">
<h4 class="section-title">
<i class="fa fa-graduation-cap me-2"></i>Education History
</h4>
<div t-if="candidate.education_history and len(candidate.education_history) > 0"
class="education-list">
<t t-foreach="candidate.education_history" t-as="edu">
<div class="education-item animate__animated animate__fadeInRight"
t-att-data-delay="edu_index * 100">
<div class="edu-header">
<h5 t-esc="edu.name or 'No degree'"/>
<span class="edu-type" t-esc="edu.education_type or 'No type'"/>
</div>
<div class="edu-university" t-esc="edu.university or 'No university'"/>
<div class="edu-duration">
<i class="fa fa-calendar me-1"></i>
<span t-esc="edu.start_year or 'Start year not specified'"/>
<span>to</span>
<span t-esc="edu.end_year or 'Present'"/>
</div>
<div class="edu-marks" t-if="edu.marks_or_grade">
<i class="fa fa-award me-1"></i>
<span t-esc="edu.marks_or_grade"/>
</div>
</div>
</t>
</div>
<t t-else="">
<div class="alert alert-info animate__animated animate__fadeIn">No education history recorded
</div>
</t>
</div>
<div class="ats-card span-12" id="candidate-family">
<h4 class="section-title">
<i class="fa fa-users me-2"></i>Family Details
</h4>
<div t-if="candidate.family_details and len(candidate.family_details) > 0"
class="family-list">
<t t-foreach="candidate.family_details" t-as="member">
<div class="family-item animate__animated animate__fadeInUp"
t-att-data-delay="member_index * 50">
<div class="family-header">
<h5 t-esc="member.name or 'No name'"/>
<span class="family-relation" t-esc="member.relation_type or 'No relation specified'"/>
</div>
<div class="family-details">
<div class="detail-row">
<span class="detail-label">Contact:</span>
<span class="detail-value" t-esc="member.contact_no or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Date of Birth:</span>
<span class="detail-value" t-esc="member.dob or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Location:</span>
<span class="detail-value" t-esc="member.location or 'N/A'"/>
</div>
</div>
</div>
</t>
</div>
<t t-else="">
<div class="alert alert-info animate__animated animate__fadeIn">No family details recorded
</div>
</t>
</div>
</div>
</template>
<template id="candidate_form_template">
<div id="candidate-form-modal" class="candidate-form-modal">
<div class="candidate-form-content">
<div class="candidate-form-header">
<div class="header-icon-container">
<i class="fas fa-user-tie header-icon"></i>
<h3>Candidate Profile</h3>
</div>
<span class="candidate-form-close">&amp;times;</span>
</div>
<div class="candidate-form-body">
<form id="candidate-form" class="candidate-form">
<!-- Header Section with Avatar and Basic Info -->
<div class="form-section header-section">
<div class="avatar-container">
<div class="candidate-avatar">
<img id="candidate-image" src="/web/static/src/img/placeholder.png"
alt="Candidate Avatar"/>
<div class="avatar-upload">
<i class="fas fa-camera"></i>
<input type="file" id="avatar-upload" accept="image/*"/>
</div>
</div>
<div class="basic-info">
<div class="form-group">
<label for="candidate-sequence">Candidate ID</label>
<input type="text" id="candidate-sequence" class="form-input" readonly="1"/>
</div>
<div class="form-group">
<label for="partner-name">Full Name*</label>
<input type="text" id="partner-name" class="form-input" required="1"
placeholder="Candidate's Name"/>
</div>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="form-section contact-section">
<h4 class="section-title">
<i class="fas fa-address-book"></i>
Contact Information
</h4>
<div class="form-grid">
<div class="form-group">
<label for="email">Email*</label>
<input type="email" id="email" class="form-input" required="1"
placeholder="Email address"/>
</div>
<div class="form-group">
<label for="phone">Phone*</label>
<input type="tel" id="phone" class="form-input" required="1"
placeholder="Phone number"/>
</div>
<div class="form-group">
<label for="alternate-phone">Alternate Phone</label>
<input type="tel" id="alternate-phone" class="form-input"
placeholder="Alternate phone"/>
</div>
<div class="form-group">
<label for="linkedin">LinkedIn Profile</label>
<input type="url" id="linkedin" class="form-input" placeholder="LinkedIn URL"/>
</div>
<div class="form-group">
<t t-set="type_ids" t-value="request.env['hr.recruitment.degree'].search([])"/>
<label for="type">Degree</label>
<select id="type" class="form-select"
data-placeholder="Select Degree" draggable="true">
<option value="">Select type</option>
<t t-foreach="type_ids" t-as="type_id" t-key="type_id.id">
<option t-att-value="type_id.id">
<t t-esc="type_id.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<t t-set="recruitment_users" t-value="request.env['res.users'].sudo().search([('groups_id', 'in', request.env.ref('hr_recruitment.group_hr_recruitment_manager').id)])"/>
<label for="manager">Manager</label>
<select id="manager" class="form-select select2"
data-placeholder="Select Manager">
<option value="">Select Manager</option>
<t t-foreach="recruitment_users" t-as="user_id" t-key="user_id.id">
<option t-att-value="user_id.id"
t-att-data-image="'/web/image/res.users/' + str(user_id.id) + '/image_128'">
<t t-esc="user_id.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="availability">Availability</label>
<input type="date" id="availability" class="form-input" placeholder="Availability"/>
</div>
</div>
</div>
<!-- Skills Section -->
<div class="form-section skills-container" id="skills-container">
<t t-set="skills" t-value="request.env['hr.skill'].search([])"/>
<label>Skills</label>
<select id="candidate-skills" class="form-select select2" multiple="multiple"
data-placeholder="Select skills" draggable="true">
<t t-foreach="skills" t-as="skill" t-key="skill.id">
<option t-att-value="skill.id">
<t t-esc="skill.display_name"/>
</option>
</t>
</select>
</div>
<!-- Resume Section -->
<div class="form-section resume-section">
<h4 class="section-title">
<i class="fas fa-file-alt"></i>
Resume
</h4>
<div class="resume-upload-container">
<div class="resume-upload-area" id="resume-dropzone">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<h5>Upload Resume</h5>
<p>Drag &amp; drop your resume here or click to browse</p>
<input type="file" id="resume-upload"
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"/>
</div>
<div class="resume-preview" id="resume-preview">
<div class="resume-preview-placeholder">
<i class="fas fa-file-alt"></i>
<p>Resume preview will appear here</p>
</div>
<iframe id="resume-iframe" style="display: none;"></iframe>
<img id="resume-image" style="display: none; max-width: 100%; max-height: 400px;"/>
<div id="unsupported-format" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<p>Preview not available for this file type</p>
<a id="download-resume" href="#" class="btn btn-primary mt-2">
<i class="fas fa-download"></i>
Download File
</a>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="form-actions">
<button type="button" class="btn btn-candidate-primary" id="upload-applicant-resume">
<i class="fas fa-upload"></i>
Upload Resume
</button>
<div class="footer-right">
<button type="button" class="btn-cancel">Cancel</button>
<button type="button" class="btn-candidate-primary btn-submit" id="save-candidate">
<i class="fas fa-save"></i>
Save Candidate
</button>
</div>
</div>
</div>
</div>
</template>
</odoo>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_candidate_view_tree_inherit_upload_doc" model="ir.ui.view">
<field name="name">hr.candidate.list.inherit.upload.doc</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="js_class">button_in_tree</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,864 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Job List Partial -->
<template id="job_list_partial_page">
<div class="ats-list-container">
<!-- Search -->
<!-- Dynamic Job Stats Header -->
<!-- Job List and Details -->
<div class="ats-list-body">
<!-- Job List Panel -->
<div class="ats-list-left expanded" id="job-list-panel">
<div class="ats-list-search">
<input type="text" id="ats-search" placeholder="🔍 Search Jobs..."/>
</div>
<!-- Toggle Button -->
<button id="job-list-sidebar-toggle-btn" class="ats-list-toggle-btn"></button>
<!-- Action Buttons Header -->
<div class="ats-actions-header">
<button class="btn" type="button" id="activeRecords" style="left:0;">
Active Records (
<span id="active-records-count">
<t t-esc="len(jobs)"/>
</span>
)
</button>
<button class="btn add-create-btn" type="button" id="add-jd-create-btn" style="right:0;">
<span class="plus-icon">+</span>
Add New JD
</button>
</div>
<div class="job-stats-header" id="job-stats-header">
<div class="job-stats-values">
<span class="badge badge-primary stat-toggle active" data-type="recruit">To Submit
</span>
<span class="badge badge-warning stat-toggle active" data-type="submitted">Submitted
</span>
<span class="badge badge-success stat-toggle active" data-type="hired">Hired</span>
<span class="badge badge-danger stat-toggle active" data-type="rejected">Rejected</span>
</div>
</div>
<ul class="ats-list">
<t t-foreach="jobs" t-as="job">
<li class="job-item ats-item"
t-att-data-id="job.id"
t-att-data-recruit="str(job.no_of_eligible_submissions or 0)"
t-att-data-submitted="str(job.no_of_submissions or 0)"
t-att-data-hired="str(job.no_of_hired_employee or 0)"
t-att-data-rejected="str(job.no_of_refused_submissions or 0)">
<div class="ats-title">
<strong>
<t t-if="job.display_name">
<t t-esc="job.display_name"/>
</t>
<t t-else="">
<t t-esc="job.job_id.display_name"/>
</t>
</strong>
</div>
<div class="ats-meta">
<span>
<t t-esc="job.job_category.display_name"/>
</span>
</div>
<div class="ats-badges">
<span class="badge badge-primary job-badge" data-type="recruit">
<t t-esc="job.no_of_eligible_submissions or 0"/>
</span>
<span class="badge badge-warning job-badge" data-type="submitted">
<t t-esc="job.no_of_submissions or 0"/>
</span>
<span class="badge badge-success job-badge" data-type="hired">
<t t-esc="job.no_of_hired_employee or 0"/>
</span>
<span class="badge badge-danger job-badge" data-type="rejected">
<t t-esc="job.no_of_refused_submissions or 0"/>
</span>
</div>
</li>
</t>
</ul>
</div>
<!-- Job Detail Panel -->
<div id="job-detail" class="ats-detail">
<em>Select a job to view details.</em>
</div>
</div>
</div>
<t t-call="hr_recruitment_web_app.add_jd_modal_template"/>
<t t-call="hr_recruitment_web_app.matching_candidates_popup"/>
<t t-call="hr_recruitment_web_app.applicants_popup"/>
</template>
<template id="add_jd_modal_template">
<div id="add-jd-modal" class="new-jd-container jd-modal">
<div class="jd-modal-content">
<div class="jd-modal-header">
<div class="header-content">
<i class="fas fa-file-alt header-icon"></i>
<h3>Create New Job Description</h3>
</div>
<span class="jd-modal-close">&amp;times;</span>
</div>
<div class="jd-modal-body">
<form id="jd-form" class="jd-creation-form">
<!-- Job Position and ID Section -->
<div class="form-section profile-section">
<h4 class="section-title">
<i class="fas fa-id-card"></i>
Job Identification
</h4>
<div class="form-grid">
<div class="form-group">
<label for="job-sequence">Unique ID</label>
<input type="text" id="job-sequence" class="form-input"
placeholder="Unique ID" required=""/>
</div>
<div class="form-group">
<label for="job-position">Job Position</label>
<select id="job-position" class="form-select select2" required="" data-placeholder="Select Job Position">
<t t-set="all_jobs" t-value="request.env['hr.job'].search([])"/>
<option value="">Select Job Position</option>
<t t-foreach="all_jobs" t-as="job">
<option t-att-value="job.id">
<t t-esc="job.display_name"/>
</option>
</t>
</select>
</div>
</div>
</div>
<!-- Section 1: Basic Information -->
<div class="form-section profile-section">
<h4 class="section-title">
<i class="fas fa-info-circle"></i>
Basic Information
</h4>
<div class="form-grid">
<div class="form-group">
<label for="job-category">Category</label>
<select id="job-category" class="form-select" required="">
<t t-set="categories" t-value="request.env['job.category'].search([])"/>
<t t-foreach="categories" t-as="category">
<option t-att-value="category.id">
<t t-esc="category.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="job-priority">Priority</label>
<select id="job-priority" class="form-select" required="">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label>Work Type</label>
<div class="radio-group">
<div class="radio-option">
<input class="form-check-input" type="radio" name="work-type"
id="work-type-internal" value="internal" checked=""/>
<label class="form-check-label" for="work-type-internal">
In-House
</label>
</div>
<div class="radio-option">
<input class="form-check-input" type="radio" name="work-type"
id="work-type-external" value="external"/>
<label class="form-check-label" for="work-type-external">
Client-Side
</label>
</div>
</div>
</div>
</div>
<div class="client-session">
<div class="form-grid">
<div class="form-group">
<label for="client-company">Client Company</label>
<select id="client-company" class="form-select select2" required=""
data-placeholder="Select Client Company">
</select>
</div>
<div class="form-group">
<label for="client-id">Client</label>
<select id="client-id" class="form-select select2" required=""
data-placeholder="Select Client">
</select>
</div>
</div>
</div>
</div>
<!-- Section 2: Employment Details -->
<div class="form-section personal-section">
<h4 class="section-title">
<i class="fas fa-file-signature"></i>
Employment Details
</h4>
<div class="form-grid">
<div class="form-group">
<label for="employment-type">Employment Type</label>
<select id="employment-type" class="form-select" required="">
<t t-set="emp_types" t-value="request.env['hr.contract.type'].search([])"/>
<t t-foreach="emp_types" t-as="emp_type">
<option t-att-value="emp_type.id">
<t t-esc="emp_type.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="job-budget">Budget</label>
<input id="job-budget" class="form-input" placeholder="Enter budget"/>
</div>
<div class="form-group">
<label for="job-no-of-positions">Positions</label>
<input id="job-no-of-positions" class="form-input" type="number" min="0"
placeholder="Number of positions" required=""/>
</div>
<div class="form-group">
<label for="job-eligible-submissions">Eligible Submissions</label>
<input id="job-eligible-submissions" class="form-input" type="number" min="0"
placeholder="Eligible Submissions" required=""/>
</div>
<div class="form-group">
<label for="target-from">Target From</label>
<input type="date" id="target-from" class="form-input"/>
</div>
<div class="form-group">
<label for="target-to">Target To</label>
<input type="date" id="target-to" class="form-input"/>
</div>
</div>
</div>
<!-- Section 3: Skills & Requirements -->
<div class="form-section professional-section">
<h4 class="section-title">
<i class="fas fa-tasks"></i>
Skills &amp; Requirements
</h4>
<t t-set="skills" t-value="request.env['hr.skill'].search([])"/>
<div class="form-grid" id="skill_requirements">
<div class="form-group">
<label for="job-primary-skills">Primary Skills</label>
<select id="job-primary-skills" class="form-select select2" multiple="multiple"
data-placeholder="Select primary skills" draggable="true">
<t t-foreach="skills" t-as="skill" t-key="skill.id">
<option t-att-value="skill.id">
<t t-esc="skill.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="job-secondary-skills">Secondary Skills</label>
<select id="job-secondary-skills" class="form-select select2" multiple="multiple"
data-placeholder="Select secondary skills" draggable="true">
<t t-foreach="skills" t-as="skill" t-key="skill.id">
<option t-att-value="skill.id">
<t t-esc="skill.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="job-experience">Experience</label>
<select id="job-experience" class="form-select">
<t t-set="job_experience" t-value="request.env['candidate.experience'].search([])"/>
<t t-foreach="job_experience" t-as="experience">
<option t-att-value="experience.id">
<t t-esc="experience.display_name"/>
</option>
</t>
</select>
</div>
</div>
</div>
<!-- Section 4: Recruitment Team -->
<div class="form-section address-section">
<h4 class="section-title">
<i class="fas fa-users-cog"></i>
Recruitment Team
</h4>
<div class="recruitment-team">
<div class="form-grid">
<div class="form-group">
<label for="primary-recruiter">Primary Recruiter</label>
<select id="primary-recruiter" class="form-select select2"
data-placeholder="Choose recruiter">
<t t-set="user_ids"
t-value="request.env['res.users'].sudo().search([])"/>
<t t-foreach="user_ids" t-as="user">
<option t-att-value="user.id"
t-att-data-image="'/web/image/res.users/' + str(user.id) + '/image_128'">
<t t-esc="user.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="secondary-recruiter">Secondary Recruiter</label>
<select id="secondary-recruiter" class="form-select select2"
multiple="multiple" data-placeholder="Select recruiters">
<t t-set="user_ids" t-value="request.env['res.users'].sudo().search([])"/>
<t t-foreach="user_ids" t-as="user">
<option t-att-value="user.id"
t-att-data-image="'/web/image/res.users/' + str(user.id) + '/image_128'">
<t t-esc="user.display_name"/>
</option>
</t>
</select>
</div>
</div>
</div>
</div>
<!-- Section 5: Additional Information -->
<div class="form-section jd-section">
<h4 class="section-title">
<i class="fas fa-info-circle"></i>
Additional Information
</h4>
<div class="additional-info">
<div class="form-grid">
<div class="form-group">
<label for="job-locations">Locations</label>
<select id="job-locations" class="form-select select2" multiple="multiple"
data-placeholder="Select job locations">
<t t-set="location_ids" t-value="request.env['hr.location'].search([])"/>
<t t-foreach="location_ids" t-as="location">
<option t-att-value="location.id">
<t t-esc="location.display_name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="recruitment-stages">Recruitment Stages</label>
<select id="recruitment-stages" class="form-select select2" multiple="multiple"
data-placeholder="Select stages">
<t t-set="recruitment_stages"
t-value="request.env['hr.recruitment.stage'].search([])"/>
<t t-foreach="recruitment_stages" t-as="stage">
<option t-att-value="stage.id"
t-att-selected="'selected' if stage.is_default_field else None">
<t t-esc="stage.display_name"/>
</option>
</t>
</select>
</div>
</div>
</div>
</div>
<!-- Job Description Section -->
<div class="form-section description-section">
<h4 class="section-title">
<i class="fas fa-align-left"></i>
Job Description
</h4>
<div class="form-group full-width">
<div id="job-description-editor" class="oe_editor"
style="min-height: 300px; border: 1px solid #ddd; padding: 8px;"></div>
<textarea id="job-description" name="description" style="display:none;"></textarea>
</div>
</div>
</form> </div>
<div class="form-actions">
<button type="button" class="btn btn-jd-primary" id="upload-jd">
<i class="fas fa-upload"></i>
Upload JD
</button>
<div class="footer-right">
<button type="button" class="btn-cancel">
Cancel
</button>
<button type="button" class="btn-jd-primary" id="save-jd">
<i class="fas fa-save"></i>
Save Job Description
</button>
</div>
</div>
</div>
</div>
</template>
<!-- Job Detail Partial - Web JD Version -->
<template id="job_detail_partial">
<div class="ats-grid job-detail-container" id="ats-details-container" t-att-data-job-id="job.id">
<!-- Close button -->
<button type="button" class="close-detail" aria-label="Close">
<span aria-hidden="true">&amp;times;</span>
</button>
<!-- Smart Button -->
<div class="smart-button-container">
<button class="btn btn-primary smart-button" data-popup-type="matchingCandidates" data-popup-title="Matching Candidates" id="matching-jd-candidates">
<i class="fa fa-magic me-1"></i> Matching Candidates
</button>
<button class="btn btn-primary smart-button" data-popup-type="applicants" data-popup-title="Applicants" id="jd-applicants">
<i class="fa fa-magic me-1"></i> Applicants
</button>
</div>
<!-- Published Ribbon -->
<div t-att-class="'status-ribbon ribbon-' + ('published' if job.website_published else 'not-published')">
<t t-esc="'Published' if job.website_published else 'Not Published'"/>
</div>
<!-- Navigation Sidebar -->
<div class="ats-card span-3" id="ats-overview">
<h4 class="section-title">
<i class="fa fa-list-ul me-2"></i>Quick Nav
</h4>
<div class="overview-nav">
<ul class="nav-list">
<li>
<a href="#ats-header" class="nav-link smooth-scroll"><i class="fa fa-info-circle me-2"></i>Job Overview</a>
</li>
<li>
<a href="#ats-basic-info" class="nav-link smooth-scroll"><i class="fa fa-info me-2"></i>Basic Information</a>
</li>
<li>
<a href="#ats-requirements" class="nav-link smooth-scroll"><i class="fa fa-tasks me-2"></i>Requirements</a>
</li>
<li>
<a href="#ats-request-info" class="nav-link smooth-scroll"><i class="fa fa-user-tie me-2"></i>Request Information</a>
</li>
<li>
<a href="#ats-team" class="nav-link smooth-scroll"><i class="fa fa-users me-2"></i>Recruitment Team</a>
</li>
<li>
<a href="#ats-description" class="nav-link smooth-scroll"><i class="fa fa-file-text me-2"></i>Job Description</a>
</li>
</ul>
</div>
</div>
<!-- Job Header Section -->
<div class="ats-card span-9" id="ats-header">
<h2 class="ats-title">
<span t-esc="job.job_id.display_name"/>
</h2>
<div class="ats-meta">
<span class="meta-item">
<i class="fa fa-briefcase me-1"></i>
<span t-esc="job.job_category.display_name"/>
</span>
<span class="meta-item">
<i class="fa fa-map-marker me-1"></i>
<span t-if="job.address_id" t-esc="job.address_id.display_name"/>
<span t-else="">Remote</span>
</span>
<span class="meta-item">
<i class="fa fa-clock-o me-1"></i>
<t t-if="job.recruitment_type == 'internal'">In-House</t>
<t t-elif="job.recruitment_type == 'external'">Client-Side</t>
<t t-else="">Unknown</t>
</span>
<span class="meta-item">
<i class="fa fa-star me-1"></i>
<span t-esc="job.job_priority"/>
</span>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="recruitment-status">
<span class="status-label">Recruitment Status:</span>
<span class="status-value" t-esc="job.recruitment_status"/>
</div>
<div class="recruitment-progress">
<div class="progress">
<div class="progress-bar" role="progressbar"
t-att-style="'width: ' + str(int(progress)) + '%;'"
t-att-aria-valuenow="progress"
aria-valuemin="0" aria-valuemax="100">
<t t-esc="str(int(progress)) + '%'"/>
</div>
</div>
<br/>
<br/>
<small class="text-muted">
<t t-esc="job.no_of_hired_employee or 0"/>
of
<t t-esc="job.no_of_recruitment or 0"/>
positions filled
</small>
</div>
</div>
</div>
<!-- Basic Information Section -->
<div class="ats-card span-6" id="ats-basic-info">
<h4 class="section-title">
<i class="fa fa-info-circle me-2"></i>Basic Information
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Employment Type:</span>
<span class="detail-value" t-esc="job.contract_type_id.display_name or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Budget:</span>
<span class="detail-value" t-esc="job.budget or 'Not specified'"/>
</div>
<div class="detail-row">
<span class="detail-label">Target From:</span>
<span class="detail-value" t-esc="job.target_from or 'N/A'"/>
<t t-if="job.target_to">
-
<span class="detail-value" t-esc="job.target_to or 'N/A'"/>
</t>
</div>
<div class="detail-row">
<span class="detail-label">Number of Positions:</span>
<span class="detail-value" t-esc="job.no_of_recruitment or 'Not specified'"/>
</div>
<div class="detail-row">
<span class="detail-label">Eligible Submissions:</span>
<span class="detail-value" t-esc="job.no_of_eligible_submissions or 'Not specified'"/>
</div>
</div>
</div>
<!-- Requirements Section -->
<div class="ats-card span-6" id="ats-requirements">
<h4 class="section-title">
<i class="fa fa-tasks me-2"></i>Requirements
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Experience:</span>
<span class="detail-value" t-esc="job.experience.display_name or 'Not specified'"/>
</div>
<div class="detail-row">
<span class="detail-label">Primary Skills:</span>
<div class="skills-list">
<t t-foreach="job.skill_ids" t-as="skill">
<span class="skill-badge" t-esc="skill.display_name"/>
</t>
<t t-if="not job.skill_ids">Not specified</t>
</div>
</div>
<div class="detail-row">
<span class="detail-label">Secondary Skills:</span>
<div class="skills-list">
<t t-foreach="job.secondary_skill_ids" t-as="skill">
<span class="skill-badge" t-esc="skill.display_name"/>
</t>
<t t-if="not job.secondary_skill_ids">Not specified</t>
</div>
</div>
<div class="detail-row">
<span class="detail-label">Locations:</span>
<div class="skills-list">
<t t-foreach="job.locations" t-as="location">
<span class="location-badge" t-esc="location.display_name"/>
</t>
<t t-if="not job.locations">Not specified</t>
</div>
</div>
</div>
</div>
<!-- Request Information Section -->
<div class="ats-card span-6" id="ats-request-info">
<h4 class="section-title">
<i class="fa fa-user-tie me-2"></i>Request Information
</h4>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Requested By:</span>
<span class="detail-value" t-esc="job.requested_by.display_name or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Department:</span>
<span class="detail-value" t-esc="job.department_id.display_name or 'N/A'"/>
</div>
<div class="detail-row">
<span class="detail-label">Company:</span>
<span class="detail-value" t-esc="job.address_id.display_name or 'N/A'"/>
</div>
</div>
</div>
<!-- Recruitment Team Section -->
<div class="ats-card span-6" id="ats-team">
<h4 class="section-title">
<i class="fa fa-users me-2"></i>Recruitment Team
</h4>
<div class="team-member">
<div class="member-header">
<i class="fa fa-user-circle me-2"></i>
<span class="member-title">Primary Recruiter</span>
</div>
<div class="member-details">
<t t-if="job.user_id">
<div class="recruiter-inline d-flex align-items-center">
<img t-att-src="'/web/image/res.users/' + str(job.user_id.id) + '/image_128'"
class="rounded-circle recruiter-photo me-3"
alt="Recruiter Photo"/>
<div>
<div class="member-name" t-esc="job.user_id.display_name"/>
<div class="member-email">
<i class="fa fa-envelope me-1"></i>
<span t-esc="job.user_id.email"/>
</div>
</div>
</div>
</t>
<t t-else="">
<div class="alert alert-warning">No primary recruiter assigned</div>
</t>
</div>
</div>
<div class="team-members">
<div class="member-header mb-3">
<i class="fa fa-users me-2"></i>
<span class="member-title">Secondary Recruiters</span>
</div>
<t t-if="job.interviewer_ids">
<div class="row row-cols-1 row-cols-md-2 g-4">
<t t-foreach="job.interviewer_ids" t-as="interviewer">
<div class="col">
<div class="team-member recruiter-inline">
<img t-att-src="'/web/image/res.users/' + str(interviewer.id) + '/image_128'"
class="rounded-circle recruiter-photo"
alt="Recruiter Photo"/>
<div>
<div class="member-name" t-esc="interviewer.display_name"/>
<div class="member-email">
<i class="fa fa-envelope me-1"></i>
<span t-esc="interviewer.email"/>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-else="">
<div class="alert alert-info">No secondary recruiters assigned</div>
</t>
</div>
</div>
<!-- Job Description Section -->
<div class="ats-card span-12" id="ats-description">
<h4 class="section-title">
<i class="fa fa-file-text me-2"></i>Job Description
</h4>
<div class="description-content">
<t t-if="job.description">
<t t-raw="job.description"/>
</t>
<t t-else="">
<div class="alert alert-info">No description provided for this job.</div>
</t>
</div>
</div>
</div>
</template>
<template id="matching_candidates_popup">
<div class="popup-container bottom-right" id="matchingCandidatesPopup">
<div class="popup-header">
<h5>Matching Candidates</h5>
<button type="button" class="popup-close">&amp;times;</button>
</div>
<div class="popup-body">
<div class="loading-spinner">
<div class="spinner"></div>
</div>
<div class="popup-content" style="display: none;"></div>
</div>
</div>
</template>
<template id="applicants_popup">
<div class="popup-container bottom-right" id="applicantsPopup">
<div class="popup-header">
<h5>Applicants</h5>
<button type="button" class="popup-close">&amp;times;</button>
</div>
<div class="popup-body">
<div class="loading-spinner">
<div class="spinner"></div>
</div>
<div class="popup-content" style="display: none;"></div>
</div>
</div>
</template>
<!-- Matching Candidates Content Template -->
<template id="matching_candidates_content">
<div class="matching-candidates-container">
<!-- Search Box Only -->
<div class="mc-search-box">
<input type="text" id="mc-search" placeholder="Search candidates..." class="mc-search-input"/>
<span class="mc-search-icon">🔍</span>
<span class="mc-match-threshold">
Showing matches ≥ <t t-esc="min_match"/>%
</span>
</div>
<div class="mc-match-threshold" style="padding: 10px; text-align: center;">
Active Records (<t t-esc="len(candidates)"/>)
</div>
<!-- Candidates Grid -->
<div class="mc-cards-container">
<t t-foreach="candidates" t-as="candidate">
<div class="mc-card"
t-att-data-id="candidate.id"
t-att-data-candidate = "candidate.display_name"
t-att-data-image = "candidate.candidate_image"
t-att-data-email = "match_data[candidate.id]['candidate_data']['email']"
t-att-data-phone = "match_data[candidate.id]['candidate_data']['phone']"
t-att-data-manager = "match_data[candidate.id]['candidate_data']['manager']"
t-att-data-applications = "match_data[candidate.id]['candidate_data']['applications']"
t-att-data-primary-percent = "match_data[candidate.id]['primary_pct']"
t-att-data-secondary-percent = "match_data[candidate.id]['secondary_pct']"
t-att-data-match-primary-skills = "match_data[candidate.id]['matched_primary_skills']"
t-att-data-match-secondary-skills = "match_data[candidate.id]['matched_secondary_skills']"
t-on-click="onCandidateClick">
<!-- Candidate Avatar with Percentage Circles -->
<div class="mc-avatar-wrapper">
<div class="mc-avatar">
<t t-if="candidate.candidate_image">
<img t-att-src="image_data_uri(candidate.candidate_image)"
class="mc-avatar-img" alt="Candidate"/>
</t>
<t t-else="">
<div class="mc-avatar-initials">
<t t-esc="candidate.display_name[0].upper() if candidate.display_name else '?'"/>
</div>
</t>
</div>
<h3 class="mc-detail-name">
<t t-esc="candidate.display_name or 'Unnamed Candidate'"/>
</h3>
<!-- Percentage Circles -->
<div class="mc-percentage-circles">
<div class="mc-percentage-circle mc-primary-circle"
t-att-data-percent="match_data[candidate.id]['primary_pct']">
<span class="mc-percentage-value">
<t t-esc="int(match_data[candidate.id]['primary_pct'])"/>%
</span>
<span class="mc-percentage-label">Primary</span>
</div>
<div class="mc-percentage-circle mc-secondary-circle"
t-att-data-percent="match_data[candidate.id]['secondary_pct']">
<span class="mc-percentage-value">
<t t-esc="int(match_data[candidate.id]['secondary_pct'])"/>%
</span>
<span class="mc-percentage-label">Secondary</span>
</div>
</div>
</div>
</div>
</t>
</div>
<!-- Empty State -->
<t t-if="not candidates">
<div class="mc-empty-state">
<div class="mc-empty-icon">😕</div>
<h4>No matching candidates found</h4>
<p>Try lowering the match threshold or expanding your search criteria</p>
</div>
</t>
<!-- Candidate Detail Modal (Hidden by default) -->
<div id="candidateDetailModal" class="mc-modal">
<div class="mc-modal-content">
<span class="mc-close-modal">&amp;times;</span>
<div id="candidateDetailContent"></div>
</div>
</div>
</div>
</template>
<!-- Applicants Content Template -->
<template id="applicants_content">
<div class="applicants-container">
<h6>Applicants for <t t-esc="job_title"/> (<t t-esc="total_applicants"/>)</h6>
<div class="stages-container">
<t t-foreach="stages" t-as="stage">
<div class="stage-card mb-3">
<h6><t t-esc="stage['name']"/> (<t t-esc="len(stage['applicants'])"/>)</h6>
<div class="applicant-list">
<t t-foreach="stage['applicants']" t-as="applicant">
<div class="applicant-card">
<div class="applicant-info">
<strong><t t-esc="applicant['name']"/></strong>
<div class="text-muted small">
Applied: <t t-esc="applicant['application_date']"/>
</div>
</div>
<div class="applicant-actions">
<a t-att-href="applicant['resume_url']"
target="_blank"
class="btn btn-sm btn-outline-primary"
t-if="applicant['resume_url'] != '#'">
View Resume
</a>
<span class="text-muted" t-if="applicant['resume_url'] == '#'">
No Resume
</span>
</div>
</div>
</t>
</div>
</div>
</t>
</div>
</div>
</template>
<!-- Error Template -->
<template id="error_template">
<div class="alert alert-danger">
<t t-esc="message"/>
</div>
</template>
</odoo>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="ats_main_home_page" name="ATS Custom UI">
<t t-name="hr_recruitment_web_app.ats_main_home_page">
<html>
<head>
<title>ATS</title>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/colors.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- <script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/sortable.min.js"></script>-->
<script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/select2.min.css"/>
<script type="text/javascript"
src="/hr_recruitment_web_app/static/src/js/select2.min.js"></script>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/ats.css"/>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/list.css?v=34"/>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/content.css?v=5"/>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/jd.css"/>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/applicants.css?v=3"/>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/candidate.css?v=1"/>
<!-- <link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/applicants.css"/>-->
<!-- <link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/applicants_details.css"/>-->
<!-- <link ref="stylesheet" href="/hr_recruitment_web_app/static/src/css/ats_candidate.css"/>-->
<script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/ats.js"></script>
<!-- <script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/candidates.js"></script>-->
<script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/job_requests.js?v=19"></script>
<script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/applicants.js?v=8"></script>
<script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/candidates.js?v=6"></script>
<script type="text/javascript"
src="https://cdn.ckeditor.com/ckeditor5/39.0.0/classic/ckeditor.js"></script>
<link rel="stylesheet" href="/hr_recruitment_web_app/static/src/css/select2.min.css"/>
<script type="text/javascript"
src="/hr_recruitment_web_app/static/src/js/select2.main.js"></script>
</head>
<body class="ats-app">
<div class="layout-container">
<aside id="sidebar" class="sidebar expanded">
<div class="main-header">
<img src="/hr_recruitment_web_app/static/src/img/logo.jpeg"
alt="ATS Logo"
style="height: 40px; width: auto;" />
<span style="font-size: 24px; font-weight: bold;">Opsentra ATS</span>
</div>
<ul class="menu-list">
<!-- <span class="list-title">RECRUITING</span>-->
<li><a href="#jobs" data-page="job_requests"><i class="bi bi-file-earmark-text"></i> JD</a></li>
<li><a href="#applicants" data-page="applicants"><i class="bi bi-people"></i> Applicants</a></li>
<li><a href="#candidates" data-page="candidates"><i class="bi bi-person-check"></i> Candidates</a></li>
</ul>
<button id="sidebar-toggle-btn" class="toggle-btn"></button>
</aside>
<main class="content-area">
<div id="main-content"></div>
<div id="job-detail"></div>
</main>
</div>
</body>
</html>
</t>
</template>
</odoo>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="action_custom_webapp" model="ir.actions.act_url">
<field name="name">My WebApp</field>
<field name="type">ir.actions.act_url</field>
<field name="url">/myATS</field>
<field name="target">new</field> <!-- Opens in new tab -->
</record>
<menuitem
id="menu_ats_webapp_root"
name="My ATS Web App"
web_icon="hr_recruitment_web_app,static/description/banner.png"
sequence="10"
action="action_custom_webapp"/>
</odoo>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_recruitment_doc_upload_wizard" model="ir.ui.view">
<field name="name">recruitment.doc.upload.wizard.form</field>
<field name="model">recruitment.doc.upload.wizard</field>
<field name="arch" type="xml">
<form string="Upload Resumes">
<sheet>
<group>
<field name="name"/>
<field name="file_name" invisible="0"/>
<field name="file_data" widget="binary" filename="file_name"/>
<field name="mimetype" readonly="1" invisible="0" force_save="1"/>
<notebook>
<page string="Data" name="json_data">
<button name="action_fetch_json" type="object" class="btn-primary" string="Fetch Data"/>
<field name="json_data"/>
</page>
<page string="HTML Text" name="html_text">
<field name="file_html_text"/>
</page>
</notebook>
</group>
</sheet>
<footer>
<button string="Create Candidate" type="object" name="action_upload" class="btn-primary"/>
<button string="Cancel" type="object" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Action to trigger the wizard -->
<record id="action_recruitment_doc_upload_wizard" model="ir.actions.act_window">
<field name="name">Upload Resumes</field>
<field name="res_model">recruitment.doc.upload.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_recruitment_doc_upload_wizard"/>
<field name="target">current</field>
</record>
<menuitem
id="menu_upload_resumes"
name="Upload Resumes"
sequence="3"
parent="hr_recruitment.menu_hr_recruitment_root"
action="action_recruitment_doc_upload_wizard"
/>
</odoo>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="res_config_settings_view_form_inherited_qwen" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.account</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="40"/>
<field name="inherit_id" ref="hr_recruitment.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//block[@name='recruitment_in_app_purchases']" position="after">
<h2>Qwen API</h2>
<div class="row mt16 o_settings_container" name="performance">
<div class="col-12 col-lg-6 o_setting_box" id="qwen_api_key">
<label for="qwen_api_key"/>
<field name="qwen_api_key"/>
<div class="text-muted">
Enter Your API key of the Together.ai.
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>