recruitment web first commit
This commit is contained in:
parent
79d14a2023
commit
e88f7da488
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import web_recruitment
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,2 @@
|
|||
from . import recruitment_doc_upload_wizard
|
||||
from . import res_config_settings
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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 |
|
|
@ -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);
|
||||
|
|
@ -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>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
7
addons_extensions/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js
vendored
Normal file
7
addons_extensions/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -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&id=' + str(candidate.id) + '&field=resume&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&id=' + str(candidate.id) + '&field=resume&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">&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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">&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 & 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">&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">&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">&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">&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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue