Compare commits

..

52 Commits

Author SHA1 Message Date
raman b479fee7c9 update changes 2025-08-18 10:39:18 +05:30
raman 672551cd0a Recruitment Web UI Changes 2025-08-08 09:53:52 +05:30
raman 0b93dd1a87 ats CSS 2025-08-07 14:53:44 +05:30
Pranay e88f7da488 recruitment web first commit 2025-08-07 12:43:43 +05:30
administrator 79d14a2023 Initial commit 2025-07-29 17:57:00 +05:30
administrator 0193fe04fb Initial commit 2025-07-29 17:57:00 +05:30
administrator b10e096540 Initial commit 2025-07-29 17:56:59 +05:30
administrator a8df849cc3 Initial commit 2025-07-29 17:56:59 +05:30
administrator d154a96b2b Initial commit 2025-07-29 17:56:59 +05:30
administrator 8c6b7669a3 Initial commit 2025-07-29 17:56:59 +05:30
administrator d56eaaa306 Initial commit 2025-07-29 17:56:59 +05:30
administrator 8c4e5d82da Initial commit 2025-07-29 17:56:59 +05:30
administrator 6d9933c2a1 Initial commit 2025-07-29 17:56:59 +05:30
administrator 2084217fbc Initial commit 2025-07-29 17:56:59 +05:30
administrator 2aef56be49 Initial commit 2025-07-29 17:56:59 +05:30
administrator 4194fbfecf Initial commit 2025-07-29 17:56:59 +05:30
administrator 95a50af0f3 Initial commit 2025-07-29 17:56:59 +05:30
administrator a64a2f8016 Initial commit 2025-07-29 17:56:59 +05:30
administrator 4f4b7e0c36 Initial commit 2025-07-29 17:56:59 +05:30
administrator 6b97411909 Initial commit 2025-07-29 17:56:59 +05:30
administrator 2896e9e83b Initial commit 2025-07-29 17:56:59 +05:30
administrator 785a7da404 Initial commit 2025-07-29 17:56:59 +05:30
administrator c21f46bf33 Initial commit 2025-07-29 17:56:59 +05:30
administrator d4f31b5af5 Initial commit 2025-07-29 17:56:59 +05:30
administrator 1e9485bb5a Initial commit 2025-07-29 17:56:59 +05:30
administrator 541272a7c1 pull commit 2025-07-29 17:56:59 +05:30
administrator 5b0d3f4a3e Initial commit 2025-07-29 17:56:59 +05:30
Pranay b195382233 TimeOff Fix 2025-07-29 17:56:59 +05:30
Pranay 709e17ad7b time-off FIX 2025-07-29 17:56:59 +05:30
Pranay b9c7d43541 Recruitment Changes 2025-07-29 17:56:58 +05:30
Pranay 3207b47f72 fix whatsapp 2025-07-29 17:56:58 +05:30
Pranay 5fdb26226c update whatsapp code 2025-07-29 17:56:58 +05:30
administrator 640ddcdaa5 Initial commit 2025-07-29 17:56:58 +05:30
administrator 6053b90162 Initial commit 2025-07-29 17:56:58 +05:30
administrator 54fafbb1e9 Initial commit 2025-07-29 17:56:58 +05:30
administrator f9985e9c58 Initial commit 2025-07-29 17:56:58 +05:30
administrator c5c89709e4 Initial commit 2025-07-29 17:56:58 +05:30
administrator 871ad34f7a Initial commit 2025-07-29 17:56:58 +05:30
administrator 7188c475d5 Initial commit 2025-07-29 17:56:58 +05:30
administrator e650a1f1bf Initial commit 2025-07-29 17:56:58 +05:30
administrator d9f2183c2b Initial commit 2025-07-29 17:56:58 +05:30
administrator 19f54e5ba0 Initial commit 2025-07-29 17:56:58 +05:30
administrator 3e6eb7799b Initial commit 2025-07-29 17:56:58 +05:30
administrator 4a2026faae Initial commit 2025-07-29 17:56:58 +05:30
administrator 5cdbdd75b0 Initial commit 2025-07-29 17:56:58 +05:30
administrator f7c05c7b2e Initial commit 2025-07-29 17:56:58 +05:30
administrator 6273e2fa96 Initial commit 2025-07-29 17:56:58 +05:30
administrator 7600729b8b Initial commit 2025-07-29 17:56:58 +05:30
administrator 25aa25c6f5 Initial commit 2025-07-29 17:56:58 +05:30
administrator a0961f338b Initial commit 2025-07-29 17:56:58 +05:30
administrator 196f7fb371 Initial commit 2025-07-29 17:56:56 +05:30
administrator e7a8762880 Initial commit 2025-07-29 17:56:56 +05:30
179 changed files with 23559 additions and 9774 deletions

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, SUPERUSER_ID
from . import models
from . import report
from . import wizard
# TODO: Generate Sequence For each company for Equipment
def pre_init_hook(env):
company_ids = env['res.company'].search([])
for company_id in company_ids:
sequence_id = env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id.id)])
if not sequence_id:
env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': company_id.id,
'padding': 5,
'number_increment': 1,
'company_id': company_id.id
})

View File

@ -1,25 +0,0 @@
{
"name": "QR Code on Equipment",
'category': '',
"summary": "Add QR Code on equipment .",
'license': 'LGPL-3',
"price": 00.00,
'description': """
The Equipment Management Module generates unique QR codes for each asset, offering instant details and direct Odoo profile access for seamless management.
""",
"author": "Raman Marikanti",
"depends": ['account','maintenance'],
"external_dependencies": {
'python': ['qrcode']
},
"data": [
'views/maintenance_equipment.xml',
'security/ir.model.access.csv',
'report/custom_qrcode.xml',
'wizard/equipment_label_layout_views.xml',
],
'pre_init_hook': 'pre_init_hook',
"application": True,
"installable": True,
}

View File

@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
from . import maintenance_equipment
from . import res_company

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
qr_code = fields.Binary("QR Code")
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
serial_no = fields.Char('Mfg. Serial Number', copy=False)
def action_print_qrcode_layout(self):
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
action['context'] = {'default_equipment_ids': self.ids}
return action
def generate_serial_no(self):
for equipment_id in self:
if not equipment_id.comp_serial_no:
company_id = equipment_id.company_id.id
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
if sequence_id:
data = sequence_id._next()
equipment_id.write({
'comp_serial_no': data
})

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class ResCompany(models.Model):
_inherit = 'res.company'
@api.model_create_multi
def create(self, vals):
result = super(ResCompany, self).create(vals)
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)])
if not sequence_id:
self.env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': result.id,
'padding': 5,
'number_increment': 1,
'company_id': result.id
})
return result

View File

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import custom_qrcode_generator

View File

@ -1,188 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="report_equipment_simple_label2x7">
<t t-set="qrcode_size" t-value="'height:14mm'"/>
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class="o_label_data">
<div class="text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width: 130px;margin-top: -40px;"/>
</t>
</div>
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
<span class="text-nowrap" t-field="equipment.serial_no"/>
<div class="o_label_extra_data">
<span t-field="equipment.comp_serial_no"/>
</div>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
<div class="o_label_clear"></div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label4x7">
<t t-set="barcode_size" t-value="'width:80px;'"/>
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class= "text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width:95px;padding:0px;margin-top:-10px"/>
</t>
</div>
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
<div class="o_label_data">
<strong t-field="equipment.serial_no"/>
<span t-field="equipment.comp_serial_no"/>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label2x5">
<div class="d-flex flex-column" style="
width: 87mm;
height:40mm;
border: 1.5px solid black;
border-radius: 6px;
padding: 6px;
font-family: Arial, Roboto, sans-serif;
font-size: 11px;
margin-bottom: 15px;
">
<!-- Header -->
<div class="text-center fw-bold" style="
font-size: 15px;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
white-space: nowrap;
">
FTPROTECH - Asset Management Team
</div>
<!-- Main Content -->
<div class="d-flex" style="flex: 1; padding-top: 6px;">
<!-- Left Column -->
<div class="flex-grow-1 pe-2" style="white-space: nowrap;">
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Asset Tag:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.name"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Serial Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.serial_no"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Model Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.model"/></div>
</div>
</div>
<!-- Right Column - QR Code -->
<div class="d-flex justify-content-end align-items-center" style="width: 30%;">
<t t-if="equipment.name">
<div t-field="equipment.qr_code"
t-options="{'widget': 'image'}"
style="width: 90px; height: 90px; display: inline-block;">
</div>
</t>
</div>
</div>
</div>
</template>
<template id="report_quipmentlabel">
<t t-call="web.html_container">
<t t-if="columns and rows">
<t t-if="columns == 2 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
</t>
<t t-if="columns == 4 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
</t>
<t t-if="columns == 2 and rows == 5">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
</t>
<t t-foreach="range(page_numbers)" t-as="page">
<div class="o_label_sheet" t-att-style="padding_page">
<table class="my-0 table table-sm table-borderless" style="border-spacing: 5mm 3mm;">
<t t-foreach="range(rows)" t-as="row">
<tr>
<t t-foreach="range(columns)" t-as="column">
<t t-if="equipment_data">
<t t-set="current_data" t-value="equipment_data.popitem()"/>
<t t-set="equipment" t-value="current_data[0]"/>
<t t-set="table_style" t-value="'border: 1px solid black;'"/>
<t t-call="{{report_to_call}}"/>
</t>
</t>
</tr>
</t>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="maintenance_quip">
<t t-call="web.basic_layout">
<div class="page">
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
<t t-set="products" t-value="products"/>
</t>
</div>
</t>
</template>
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
<field name="name">A4 Label Sheet</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
</record>
<record id="report_equipment_label" model="ir.actions.report">
<field name="name">Equipment QR-code (PDF)</field>
<field name="model">maintenance.equipment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
<field name="binding_model_id" eval="False"/>
<field name="binding_type">report</field>
</record>
</data>
</odoo>

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import math
from io import BytesIO
import qrcode
from odoo import models
def generate_qr_code(value):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=20,
border=4,
)
qr.add_data(value)
qr.make(fit=True)
img = qr.make_image()
temp = BytesIO()
img.save(temp, format="PNG")
qr_img = base64.b64encode(temp.getvalue())
return qr_img
def _prepare_data(env, data):
equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id'])
equipment_dict = {}
equipment_ids = equipment_label_layout_id.equipment_ids
for equipment in equipment_ids:
if not equipment.name:
continue
equipment_dict[equipment] = 1
combine_equipment_detail = ""
# Generate Equipment Redirect LInk
url = env['ir.config_parameter'].sudo().get_param('web.base.url')
menuId = env.ref('maintenance.menu_equipment_form').sudo().id
actionId = env.ref('maintenance.hr_equipment_action').sudo().id
equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str(
actionId) + '&model=maintenance.equipment&view_type=form'
# Prepare main Equipment Detail
main_equipment_detail = ""
main_equipment_detail = main_equipment_detail.join(
"Name: " + str(equipment.name) + "\n" +
"Model: " + str(equipment.model) + "\n" +
"Mfg serial no: " + str(equipment.serial_no) + "\n"
"Warranty Exp. Date: " +str(equipment.warranty_date) + "\n"
"Category: " +str(equipment.category_id.name)+ "\n"
"Contact No: "+str(equipment.technician_user_id.phone) +"\n"
"Contact Email: "+str(equipment.technician_user_id.login)
)
# main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail
# Prepare Child Equipment Detail
combine_equipment_detail = main_equipment_detail
combine_equipment_detail += '\n' + '\n' + equipment_link
# Generate Qr Code depends on Details
qr_image = generate_qr_code(combine_equipment_detail)
equipment.write({
'qr_code': qr_image
})
env.cr.commit()
page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1
dict_equipment = {
'rows': equipment_label_layout_id.rows,
'columns': equipment_label_layout_id.columns,
'page_numbers': page_numbers,
'equipment_data': equipment_dict
}
return dict_equipment
class ReportProductTemplateLabel(models.AbstractModel):
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
_description = 'Equipment QR-code Report'
def _get_report_values(self, docids, data):
return _prepare_data(self.env, data)

View File

@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_equipment_label_layout access.equipment_label_layout model_equipment_label_layout 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_extend_equipment_tree" model="ir.ui.view">
<field name="name">maintenance.equipment.tree.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<header>
<button string="Print QR-code" type="object" name="action_print_qrcode_layout"/>
</header>
</xpath>
</field>
</record>
<record id="view_extend_equipment_form" model="ir.ui.view">
<field name="name">maintenance.equipment.form.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='product_information']" position="inside">
<group>
<field name="comp_serial_no"/>
</group>
</xpath>
<xpath expr="//sheet" position="before">
<header>
<button string="Generate Serial Number" type="object" name="generate_serial_no" class="oe_highlight"/>
</header>
</xpath>
</field>
</record>
<record id="generate_serial_no_action" model="ir.actions.server">
<field name="name">Generate Serial Number</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.generate_serial_no()</field>
</record>
<record id="generate_qrcode_no_action" model="ir.actions.server">
<field name="name">Print QR-Code</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">form</field>
<field name="state">code</field>
<field name="code">action = records.action_print_qrcode_layout()</field>
</record>
</odoo>

View File

@ -1,3 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import equipment_label_layout

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class EquipmentLabelLayout(models.TransientModel):
_name = 'equipment.label.layout'
_description = 'Choose the sheet layout to print the labels'
print_format = fields.Selection([
('2x5', '2 x 5'),
('2x7', '2 x 7'),
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
equipment_ids = fields.Many2many('maintenance.equipment')
rows = fields.Integer(compute='_compute_dimensions')
columns = fields.Integer(compute='_compute_dimensions')
@api.depends('print_format')
def _compute_dimensions(self):
for wizard in self:
if 'x' in wizard.print_format:
columns, rows = wizard.print_format.split('x')[:2]
wizard.columns = int(columns)
wizard.rows = int(rows)
else:
wizard.columns, wizard.rows = 1, 1
def process_label(self):
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
data = {
'equipment_label_layout_id':self.id
}
return self.env.ref(xml_id).report_action(None, data=data)

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="equipment_label_layout_form" model="ir.ui.view">
<field name="name">equipment.label.layout.form</field>
<field name="model">equipment.label.layout</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="print_format" widget="radio"/>
</group>
</group>
<footer>
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
<field name="name">Choose Labels Layout</field>
<field name="res_model">equipment.label.layout</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
<field name="target">new</field>
</record>
</odoo>

View File

@ -45,7 +45,7 @@ class HrPayslipRun(models.Model):
'attendance_days': attendance_days,
'leave_days': leave_days,
'lop_days': lop_days,
'doj':contract.date_start,
'doj':employee.doj,
'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0),

View File

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

View File

@ -1,65 +0,0 @@
from odoo import http, _
from odoo.http import request
from odoo.addons.hr_recruitment_extended.controllers.controllers import website_hr_recruitment_applications
from odoo.http import content_disposition
import logging
from odoo.tools import misc
_logger = logging.getLogger(__name__)
class website_hr_recruitment_applications_extended(website_hr_recruitment_applications):
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
website=True)
def post_onboarding_form(self, applicant_id, **kwargs):
"""Renders the website form for applicants to submit additional details."""
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found()
if applicant and applicant.send_post_onboarding_form:
if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template", {
'applicant': applicant
})
else:
return request.render("hr_recruitment_extended.post_onboarding_form_template", {
'applicant': applicant
})
else:
return request.not_found()
@http.route(['/download/jod/<int:applicant_id>'], type='http', auth="public", cors='*', website=True)
def download_jod_form(self, applicant_id, **kwargs):
# Get the applicant record
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return f"Error: Applicant with ID {applicant_id} not found"
# Business logic check
if not applicant.send_post_onboarding_form or applicant.post_onboarding_form_status != 'done':
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
# Get the template
template = request.env.ref('hr_recruitment_extended.employee_joining_form_template')
if not template:
return "Error: Template not found"
try:
# Render the template to HTML for debugging
html = request.env['ir.qweb']._render(
template.id,
{
'docs': applicant,
'doc': applicant,
'time': misc.datetime,
'user': request.env.user,
}
)
# Return HTML for debugging
return html
except Exception as e:
return f"Error rendering template: {str(e)}"

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="emp_joining_form_template">
<t t-call="web.basic_layout">
<t t-call="web.external_layout">
<main class="page"
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
<t t-foreach="docs" t-as="doc">
@ -313,13 +313,4 @@
</t>
</template>
<!-- <template id="thank_you_template_inherit" inherit_id="hr_recruitment_exteded.thank_you_template" name="Thank You Template Extended">-->
<!-- <xpath expr="//div[@class='container mt-5 text-center']" position="inside">-->
<!-- <div t-if="applicant.post_onboarding_form_status == 'done'" style="margin-top: 20px;">-->
<!-- <a t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>-->
<!-- </div>-->
<!-- </xpath>-->
<!-- </template>-->
</odoo>

View File

@ -97,8 +97,7 @@ daily_checkins AS (
at.worked_hours,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row,
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row,
dep.name->>'en_US' AS department,
LEAD(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS next_check_in
dep.name->>'en_US' AS department
FROM
hr_attendance at
LEFT JOIN
@ -116,10 +115,6 @@ attendance_summary AS (
MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in,
MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out,
SUM(worked_hours) AS total_worked_hours,
-- 👇 Calculate total break time (sum of gaps between check_out and next check_in)
SUM(
EXTRACT(EPOCH FROM (next_check_in - check_out)) / 3600
) FILTER (WHERE next_check_in IS NOT NULL AND check_out IS NOT NULL) AS break_hours,
department
FROM
daily_checkins
@ -152,7 +147,6 @@ SELECT
COALESCE(ats.first_check_in, NULL) AS first_check_in,
COALESCE(ats.last_check_out, NULL) AS last_check_out,
COALESCE(ats.total_worked_hours, 0) AS total_worked_hours,
COALESCE(ats.break_hours, 0) AS total_break_hours,
ed.department,
CASE
WHEN ld.leave_type IS NOT NULL AND ld.is_half_day THEN 'on Half day ' || ld.leave_type
@ -171,6 +165,7 @@ LEFT JOIN
ORDER BY
ed.employee_id, ed.date;
"""
# Combine all parameters in the correct order:
# 1. date_range params (start_date_str, end_date_str)
# 2. employee_dates params (emp_date_params)
@ -201,7 +196,6 @@ ORDER BY
'check_in': r['first_check_in'],
'check_out': r['last_check_out'],
'worked_hours': float(r['total_worked_hours']) if r['total_worked_hours'] is not None else 0.0,
'break_hours': float(r['total_break_hours']) if r['total_break_hours'] is not None else 0.0,
'status': r['status']
})
@ -219,6 +213,7 @@ ORDER BY
attendance_data = self.get_attendance_report(department_id, employee_id, start_date, end_date)
if not attendance_data:
raise UserError("No data to export!")
# Create workbook and sheet
workbook = xlwt.Workbook(encoding='utf-8')
sheet = workbook.add_sheet('Attendance Report')
@ -286,26 +281,28 @@ ORDER BY
)
# Set column widths (in units of 1/256 of a character width)
col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 4000, 5000]
col_widths = [6000, 8000, 7000, 3000, 4000, 5000, 5000, 4000, 5000]
for i, width in enumerate(col_widths):
sheet.col(i).width = width
# Write title
sheet.write_merge(0, 0, 0, 9, 'ATTENDANCE REPORT', title_style)
sheet.write_merge(0, 0, 0, 8, 'ATTENDANCE REPORT', title_style)
# Write date range
date_range = f"From: {start_date} To: {end_date}"
sheet.write_merge(1, 1, 0, 9, date_range, xlwt.easyxf(
sheet.write_merge(1, 1, 0, 8, date_range, xlwt.easyxf(
'font: italic on; align: horiz center'
))
# Write headers
headers = [
'Department', 'Employee Name','Week', 'Date', 'Day',
'Check-in', 'Check-out', 'Worked Hours', 'Break Hours', 'Status'
'Check-in', 'Check-out', 'Worked Hours', 'Status'
]
for col_num, header in enumerate(headers):
sheet.write(2, col_num, header, header_style)
# Write data rows
current_employee = None
for row_num, record in enumerate(attendance_data, start=3):
@ -348,16 +345,11 @@ ORDER BY
else:
sheet.write(row_num, 7, str(record['worked_hours']), data_style)
# Break hours formatting
if isinstance(record['break_hours'], (float, int)):
sheet.write(row_num, 8, float(record['break_hours']), hours_style)
else:
sheet.write(row_num, 8, str(record['break_hours']), data_style)
sheet.write(row_num, 8, record['status'], status_style)
sheet.write(row_num, 9, record['status'], status_style)
# Add freeze panes (headers will stay visible when scrolling)
sheet.set_panes_frozen(True)
sheet.set_horz_split_pos(3) # After row 3 (headers)
sheet.set_horz_split_pos(4) # After row 3 (headers)
sheet.set_vert_split_pos(0) # No vertical split
# Save to buffer

View File

@ -206,7 +206,7 @@ export default class AttendanceReport extends Component {
}
async generateReport() {
debugger;
let { startDate, endDate, selectedEmployeeIds } = this.state;
startDate = $('#from_date').val()
endDate = $('#to_date').val()
@ -231,7 +231,7 @@ export default class AttendanceReport extends Component {
// Fetch the attendance data based on the date range and selected employees
// const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']);
const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#dept').val(),$('#emp').val(),startDate,endDate]);
debugger;
// Group data by employee_id
const rawGroups = this.groupDataByEmployee(attendanceData);

View File

@ -65,7 +65,6 @@
<th>Check In</th>
<th>Check Out</th>
<th>Worked Hours</th>
<th>Break Hours</th>
<th>Status</th>
</tr>
</thead>
@ -79,16 +78,7 @@
<td><t t-esc="data.day_name"/></td>
<td><t t-esc="data.check_in"/></td>
<td><t t-esc="data.check_out"/></td>
<td>
<t t-set="hours" t-value="Math.floor(data.worked_hours)"/>
<t t-set="minutes" t-value="Math.round((data.worked_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td>
<t t-set="hours" t-value="Math.floor(data.break_hours)"/>
<t t-set="minutes" t-value="Math.round((data.break_hours - hours) * 60)"/>
<t t-esc="hours"/>:<t t-esc="minutes >= 10 ? minutes : '0' + minutes"/>
</td>
<td><t t-esc="data.worked_hours"/></td>
<td><t t-esc="data.status"/></td>
</tr>
</tbody>

View File

@ -18,8 +18,12 @@
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base','hr','account','mail','hr_skills', 'hr_contract'],
# always loaded
'data': [
'security/security.xml',

View File

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': 'Human Resources',
'version': '1.0',
'summary': 'Human Resources all',
'description': '''
Human Resources of the module
''',
'category': 'Human Resources',
'author': 'Raman Marikanti',
'depends': ['base', 'mail',
'hr_employee_extended','hr_contract','hr_payroll',
'hr_attendance_extended','hr_payroll_holidays',
'hr_recruitment_extended'],
'data': [
'security/ir.model.access.csv',
'views/hr_views.xml',
],
'license': 'LGPL-3',
'installable': True,
'application': False,
'auto_install': False,
}

View File

@ -1 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem name="Human Resources"
id="hr_menu_root"/>
<menuitem
id="menu_hr_main"
name="Employees"
parent="hr_menu_root"
sequence="0"/>
<menuitem
id="menu_hr_employee_user_hr"
name="Employees"
action="hr.open_view_employee_list_my"
parent="menu_hr_main"
sequence="1"/>
<menuitem
id="hr_menu_all_contracts_hr"
name="Contracts"
action="hr_contract.action_hr_contract"
parent="menu_hr_main"
sequence="30"/>
<menuitem
id="hr_menu_salary_attachments_hr"
name="Salary Attachments"
action="hr_payroll.hr_salary_attachment_action"
parent="menu_hr_main"
sequence="35"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
id="menu_hr_pay"
name="Payslips"
parent="hr_menu_root"
sequence="1"
groups="hr_payroll.group_hr_payroll_user"/>
<menuitem
id="menu_hr_payslip_run_hr"
action="hr_payroll.action_hr_payslip_run_tree"
name="Batches"
sequence="2"
parent="menu_hr_pay"/>
<menuitem
id="menu_hr_payroll_employee_payslips_hr"
name="All Payslips"
parent="menu_hr_pay"
sequence="1"
action="hr_payroll.action_view_hr_payslip_month_form"
groups="hr_payroll.group_hr_payroll_user"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
id="menu_hr_attendance_hr"
name="Work Management"
parent="hr_menu_root"
sequence="1"
groups="hr_payroll.group_hr_payroll_user"/>
<menuitem
id="menu_hr_attendance_view_attendances_hr"
name="Attendance" parent="menu_hr_attendance_hr"
sequence="5"
groups="hr_attendance.group_hr_attendance_officer"
action="hr_attendance.hr_attendance_action"/>
<menuitem
id="menu_open_department_leave_approve_hr"
name="Time Off"
parent="menu_hr_attendance_hr"
action="hr_holidays.hr_leave_action_action_approve_department"
sequence="55"/>
<!-- ////////////////////////////////////////////////////////////////////////////// -->
<menuitem
name="Recruitment"
id="menu_hr_recruitment_hr"
parent="hr_menu_root"
sequence="-1"/>
<menuitem
name="Applications"
parent="menu_hr_recruitment_hr"
id="menu_hr_recruitment_applications_hr"
action="hr_recruitment.crm_case_categ0_act_job"
sequence="3"/>
<menuitem
name="Candidates"
parent="menu_hr_recruitment_hr"
id="menu_hr_candidate_hr"
action="hr_recruitment.action_hr_candidate"
sequence="2"/>
</odoo>

View File

@ -456,8 +456,8 @@ class HrPayslip(models.Model):
'res_id': payslip.id
})
# Send email to employees
# if template:
# template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light')
if template:
template.send_mail(payslip.id, email_layout_xmlid='mail.mail_notification_light')
self.env['ir.attachment'].sudo().create(attachments_vals_list)
def _filter_out_of_contracts_payslips(self):
@ -1381,10 +1381,7 @@ class HrPayslip(models.Model):
def days_count(self):
days = self.worked_days_line_ids.filtered(lambda x:x.work_entry_type_id.code == 'OUT').number_of_days
joining_date = self.contract_id.date_start
if not joining_date or joining_date == self.date_from:
return 0
date_from = min(joining_date, self.date_from)
if joining_date > date_from:
@ -1403,7 +1400,7 @@ class HrPayslip(models.Model):
weekend_days_count = weekend_count
else:
weekend_days_count = 0
return weekend_days_count + days
return weekend_days_count
def action_edit_payslip_lines(self):
self.ensure_one()
@ -1852,40 +1849,3 @@ class HrPayslip(models.Model):
if 'stats' in sections:
result['stats'] = self._get_dashboard_stats()
return result
def get_leave_balance(self):
employee = self.employee_id
if not employee:
return {'error': 'No employee linked to this user'}
leave_data = {}
leave_types = self.env['hr.leave.type'].search([
])
if not leave_types:
return []
for leave_type in leave_types:
allocations = self.env['hr.leave.allocation'].search([
('employee_id', '=', employee.id),
('holiday_status_id', '=', leave_type.id),
('state', '=', 'validate'),
])
taken_leaves = self.env['hr.leave'].search([
('employee_id', '=', employee.id),
('holiday_status_id', '=', leave_type.id),
('state', 'in', ['validate','validate1','confirm']),
])
total_allocated = sum(a.number_of_days for a in allocations)
total_taken = sum(l.number_of_days for l in taken_leaves)
remaining = total_allocated - total_taken
if remaining <= 0:
continue
leave_data[leave_type.name] = {
'name':leave_type.name,
'allocated': total_allocated,
'taken': total_taken,
'remaining': remaining,
}
return leave_data

View File

@ -44,21 +44,20 @@
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
<tr>
<td style="border: 1px solid #ccc; padding: 6px;">
<u><b>Pay Summary</b></u><br/><br/>
<strong>Pay Period:</strong> <t style="padding: 6px;" t-esc="o.date_from"/> - <t t-esc="o.date_to"/><br/>
<t t-set="days" t-value="(o.date_to - o.date_from).days + 1"/>
<strong>Number of Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days<br/>
<strong>Worked Days:</strong> <t style="padding: 6px;" t-esc="days"/> Days
</td>
<t t-set="leave_data" t-value="o.get_leave_balance()"/>
<td t-if="leave_data" style="border: 1px solid #ccc; padding: 6px;">
<p><u><b>Leave Balance</b></u>
<div t-foreach="leave_data.values()" t-as="data">
<strong t-out="data['name'] + ':'"/>
<t t-out="data['remaining']"/>
Days
<t t-set="timeoff_data_table" t-value="o._get_employee_timeoff_data()"/>
<td t-if="timeoff_data_table" style="border: 1px solid #ccc; padding: 6px;">
<div t-foreach="timeoff_data_table" t-as="timeoff_data">
<strong t-out="timeoff_data[0] + ':'"/>
<t t-out="timeoff_data[1].get('remaining_leaves')"/> /
<t t-out="timeoff_data[1].get('max_leaves')"/>
<t t-if="timeoff_data[1].get('request_unit') == 'hour'">Hours</t>
<t t-else="">Days</t>
</div>
</p>
</td>
</tr>
</table>
@ -77,16 +76,12 @@
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW']and l.amount &gt; 0)" t-as="l">
<t t-esc="l.name"/><br/>
</div>
<br/>
<strong>Total Income</strong>
</td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW'] and l.amount &gt; 0)" t-as="l">
<t t-esc="'%.2f' % l.amount"/><br/>
<t t-esc="l.amount"/><br/>
<t t-set="income" t-value="income + l.amount"/>
</div>
<br/>
<strong><t t-esc="'%.2f' % income"/></strong>
</td>
<td style="border: 1px solid #ccc; padding: 6px;">
<t t-set="contribution" t-value="0"/>
@ -96,7 +91,7 @@
</td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['COMP','MA'] and l.amount &gt; 0)" t-as="l">
<t t-esc="'%.2f' % l.amount"/><br/>
<t t-esc="l.amount"/><br/>
<t t-set="contribution" t-value="contribution + l.amount"/>
</div>
</td>
@ -112,33 +107,30 @@
</div>
</td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<strong><t t-esc="'%.2f' % (contribution + income)"/></strong><br/><br/><br/>
<strong><t t-esc="contribution + income"/></strong><br/><br/><br/>
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l">
<t t-esc="'%.2f' % l.amount"/><br/>
<t t-esc="l.amount"/><br/>
<t t-set="ded" t-value="ded + l.amount"/>
</div>
</td>
</tr>
<tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Gross Salary</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % income"/></strong></td>
<td style="border: 1px solid #ccc; padding: 6px;"><strong>Gross Salary</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="income"/></strong></td>
<td style="border: 1px solid #ccc; padding: 6px;"><strong>Total Deduction</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"><strong><t t-esc="ded"/></strong></td>
</tr>
<tr>
<td style="border: 1px solid #ccc; padding: 6px;" colspan="3"><strong>Total Deduction</strong></td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;" colspan="1"><strong><t t-esc="'%.2f' % ded"/></strong></td>
</tr>
<tr>
<td class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;font-size: 14px;"><strong>Net Salary:</strong></td>
<td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;font-size: 14px;">
<strong><t t-esc="'%.2f' % (income + ded)"/></strong>
<td class="net-salary" colspan="3" style="border: 1px solid #ccc; padding: 6px;"><strong>Net Salary:</strong></td>
<td class="net-salary" colspan="1" style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<t t-esc="(contribution + income) + ded"/>
</td>
</tr>
</table>
<div class="to-pay" style="margin-top: 20px;">
<p t-if="o.net_wage &gt;= 0">
To pay <strong><span t-esc="'%.2f' % (income + ded)"/></strong> (<span style="padding-right: 5px;" t-esc="o.env.company.currency_id.amount_to_text(income + ded)"/> only) to <i><span t-field="o.employee_id.legal_name"/></i> - <b><span t-field="o.employee_id.bank_account_id.bank_id.name"/> Account : <span t-field="o.employee_id.bank_account_id.acc_number">XXXXXXXXXXXX</span></b>
To pay <strong><span t-esc="(contribution + income) + ded"/></strong> (<span style="padding-right: 5px;" t-esc="o.env.company.currency_id.amount_to_text((contribution + income) + ded) "/> only) to <i><span t-field="o.employee_id.legal_name"/></i> - <b><span t-field="o.employee_id.bank_account_id.bank_id.name"/> Account : <span t-field="o.employee_id.bank_account_id.acc_number">XXXXXXXXXXXX</span></b>
</p>

View File

@ -84,7 +84,7 @@
name="Contracts"
action="hr_contract.action_hr_contract"
parent="menu_hr_payroll_employees_root"
sequence="90"/>
sequence="30"/>
<menuitem
id="hr_menu_salary_attachments"

View File

@ -164,14 +164,13 @@ class website_hr_recruitment_applications(http.Controller):
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
methods=['POST'], website=True, csrf=False)
def process_employee_joining_form(self,applicant_id,**post):
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found() # Return 404 if applicant doesn't exist
if applicant.post_onboarding_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template",{
'applicant': applicant
})
return request.render("hr_recruitment_extended.thank_you_template")
private_state_id = request.env['res.country.state'].sudo().browse(int(post.get('present_state', 0)))
permanent_state_id = request.env['res.country.state'].sudo().browse(int(post.get('permanent_state', 0)))
@ -281,41 +280,8 @@ class website_hr_recruitment_applications(http.Controller):
]
applicant.write(applicant_data)
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
raise_if_not_found=False)
# Get HR managers with HR department
group = request.env.ref('hr.group_hr_manager')
users = request.env['res.users'].sudo().search([
('groups_id', 'in', group.ids),
('email', '!=', False),
('email', '!=', 'hr@ftprotech.com'),
('employee_id.department_id.name', '=', 'Human Resource')
])
# Extract emails and join them into a comma-separated string
email_cc = ','.join([user.email for user in users])
# Prepare email values
email_values = {
'email_from': applicant.email_from,
'email_to': 'hr@ftprotech.com',
'email_cc': email_cc
}
# Debug: Print the email_cc value to verify
print(f"Email CC value: {email_values['email_cc']}")
# Send email
template.sudo().send_mail(
applicant.id,
email_values=email_values,
force_send=True
)
# Render thank you page
return request.render("hr_recruitment_extended.thank_you_template", {
'applicant': applicant
})
return request.render("hr_recruitment_extended.thank_you_template")
def safe_date_parse(self,date_str):
try:

View File

@ -290,49 +290,6 @@
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_post_onboarding_form_user_submit" model="mail.template">
<field name="name">Joining Formalities Submission Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ object.email_from }}</field>
<field name="email_to">hr@ftprotech.com</field>
<field name="subject">{{ object.candidate_id.partner_name or 'Applicant' }} JOD Submission</field>
<field name="description">
Notification sent by the applicants with joining formalities details.
</field>
<field name="body_html" type="html">
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t>HR</t>
</strong>
,
</p>
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
<t t-if="object.employee_code">
<t t-set="employee_code" t-value="object.employee_code"/>
</t>
<p>
<t t-esc="applicant_name"/> has submitted the Joining Formalities (JOD) Form. Please click the link below to review the details.
</p>
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<!-- FIXED LINE: Using proper string concatenation -->
<t t-set="form_url" t-value="base_url + '/odoo/hr.applicant/' + str(object.id)"/>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="form_url" target="_blank" style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none; font-weight: bold; border-radius: 5px; display: inline-block;">
Open Application
</a>
</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
</record>
<record id="email_template_post_onboarding_form" model="mail.template">
<field name="name">Joining Formalities Notification</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>

View File

@ -1,7 +1,7 @@
<odoo>
<odoo>
<template id="employee_joining_form_template">
<t t-call="web.basic_layout">
<t t-call="web.external_layout">
<main class="page"
style="margin: 0px; padding: 0px; font-size: 16px; font-family: 'Arial', sans-serif;">
<t t-foreach="docs" t-as="doc">

View File

@ -256,6 +256,8 @@ class HRJobRecruitment(models.Model):
rec.submission_status = 'zero'
experience = fields.Many2one('candidate.experience', string="Experience")
@api.depends('application_ids.submitted_to_client')
def _compute_no_of_submissions(self):
counts = dict(self.env['hr.applicant']._read_group(

View File

@ -10,7 +10,7 @@ access_hr_job_recruitment_user,access.hr.job.recruitment.user,model_hr_job_recru
access_hr_job_recruitment_manager,access.hr.job.recruitment.manager,model_hr_job_recruitment,hr_recruitment.group_hr_recruitment_user,1,1,1,1
hr_recruitment.access_hr_candidate_interviewer,hr.candidate.interviewer,hr_recruitment.model_hr_candidate,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
access_hr_candidate_hr,hr.candidate.hr,hr_recruitment.model_hr_candidate,hr.group_hr_manager,1,0,0,0
access_candidate_experience,access.candidate.experience.manager,model_candidate_experience,hr_recruitment.group_hr_recruitment_user,1,1,1,1
access_candidate_experience_user,access.candidate.experience.user,model_candidate_experience,base.group_user,1,0,0,0
@ -23,13 +23,10 @@ access_employee_recruitment_attachments,employee.recruitment.attachments,model_e
hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recruitment.model_hr_applicant,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0
access_hr_recruitment_stage_hr,hr.recruitment.stage.hr,hr_recruitment.model_hr_recruitment_stage,hr.group_hr_manager,1,0,0,0
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
,,,,,,,
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_candidate_experience_user access_recruitment_attachments_user access.candidate.experience.user access.recruitment.attachments.user model_candidate_experience model_recruitment_attachments base.group_user 1 0 1 0 1 0 1
11 access_recruitment_attachments_user access_post_onboarding_attachment_wizard access.recruitment.attachments.user access.post.onboarding.attachment.wizard model_recruitment_attachments model_post_onboarding_attachment_wizard base.group_user 1 1 1 1
12 access_post_onboarding_attachment_wizard access_employee_recruitment_attachments access.post.onboarding.attachment.wizard employee.recruitment.attachments model_post_onboarding_attachment_wizard model_employee_recruitment_attachments base.group_user 1 1 1 1
13 access_employee_recruitment_attachments hr_recruitment.access_hr_applicant_interviewer employee.recruitment.attachments hr.applicant.interviewer model_employee_recruitment_attachments hr_recruitment.model_hr_applicant base.group_user hr_recruitment.group_hr_recruitment_interviewer 1 1 1 1 0
14 hr_recruitment.access_hr_applicant_interviewer hr_recruitment.access_hr_recruitment_stage_user hr.applicant.interviewer hr.recruitment.stage.user hr_recruitment.model_hr_applicant hr_recruitment.model_hr_recruitment_stage hr_recruitment.group_hr_recruitment_interviewer hr_recruitment.group_hr_recruitment_user 1 1 1 0
15 hr_recruitment.access_hr_recruitment_stage_user access_application_stage_status hr.recruitment.stage.user application.stage.status hr_recruitment.model_hr_recruitment_stage model_application_stage_status hr_recruitment.group_hr_recruitment_user base.group_user 1 1 1 0 1
16 access_hr_recruitment_stage_hr access_ats_invite_mail_template_wizard hr.recruitment.stage.hr ats.invite.mail.template.wizard.user hr_recruitment.model_hr_recruitment_stage hr_recruitment_extended.model_ats_invite_mail_template_wizard hr.group_hr_manager 1 0 1 0 1 0 1
23
24
25
26
27
28
29
30
31
32

View File

@ -465,13 +465,6 @@
<label>Permanent Address
<span class="text-danger">*</span>
</label>
<!-- Checkbox to toggle same address -->
<div class="form-check d-inline-block ml-3">
<input type="checkbox" class="form-check-input permanent-address-checkbox" id="same_as_present"/>
<label class="form-check-label" for="same_as_present">Same as Present Address</label>
</div>
</h5>
<div class="mb-3">
<input type="text" class="form-control mb-2" name="permanent_street"
@ -1723,80 +1716,6 @@
let prevButtons = document.querySelectorAll(".prev-step");
let currentStep = 0;
// Same as Present Address functionality
const sameAsPresentCheckbox = document.getElementById('same_as_present');
const form = document.getElementById('post_onboarding_form');
// Add event listener to the checkbox
sameAsPresentCheckbox.addEventListener('change', function() {
if (this.checked) {
// Get present address elements by name (since they don't have IDs)
const presentStreet = form.querySelector('input[name="present_street"]');
const presentStreet2 = form.querySelector('input[name="present_street2"]');
const presentCity = form.querySelector('input[name="present_city"]');
const presentZip = form.querySelector('input[name="present_zip"]');
// Get permanent address elements by name
const permanentStreet = form.querySelector('input[name="permanent_street"]');
const permanentStreet2 = form.querySelector('input[name="permanent_street2"]');
const permanentCity = form.querySelector('input[name="permanent_city"]');
const permanentZip = form.querySelector('input[name="permanent_zip"]');
// Copy values only if both elements exist
if (permanentStreet &amp;&amp; presentStreet) permanentStreet.value = presentStreet.value;
if (permanentStreet2 &amp;&amp; presentStreet2) permanentStreet2.value = presentStreet2.value;
if (permanentCity &amp;&amp; presentCity) permanentCity.value = presentCity.value;
if (permanentZip &amp;&amp; presentZip) permanentZip.value = presentZip.value;
// Handle state field
const presentStateContainer = document.getElementById('present_state_ids_container');
const permanentStateContainer = document.getElementById('permanent_state_ids_container');
// If state dropdowns exist, copy the selected value
const presentStateSelect = presentStateContainer ? presentStateContainer.querySelector('select') : null;
const permanentStateSelect = permanentStateContainer ? permanentStateContainer.querySelector('select') : null;
if (presentStateSelect &amp;&amp; permanentStateSelect) {
permanentStateSelect.value = presentStateSelect.value;
}
// Make permanent address fields readonly
[permanentStreet, permanentStreet2, permanentCity, permanentZip].forEach(field => {
if (field) {
field.readOnly = true;
field.classList.add('bg-light');
}
});
if (permanentStateSelect) {
permanentStateSelect.disabled = true;
permanentStateSelect.classList.add('bg-light');
}
} else {
// Make permanent address fields editable but keep the values
const permanentStreet = form.querySelector('input[name="permanent_street"]');
const permanentStreet2 = form.querySelector('input[name="permanent_street2"]');
const permanentCity = form.querySelector('input[name="permanent_city"]');
const permanentZip = form.querySelector('input[name="permanent_zip"]');
[permanentStreet, permanentStreet2, permanentCity, permanentZip].forEach(field => {
if (field) {
field.readOnly = false;
field.classList.remove('bg-light');
}
});
const permanentStateContainer = document.getElementById('permanent_state_ids_container');
const permanentStateSelect = permanentStateContainer ? permanentStateContainer.querySelector('select') : null;
if (permanentStateSelect) {
permanentStateSelect.disabled = false;
permanentStateSelect.classList.remove('bg-light');
}
}
});
function updateSteps() {
// Show/hide step content
steps.forEach((step, index) => {
@ -2230,7 +2149,6 @@
<h2>Thank You for Your Submission</h2>
<p>Your form has been successfully submitted.</p>
<a href="/" class="btn btn-primary">Go Back to Home</a>
<a t-if="applicant and applicant.post_onboarding_form_status == 'done'" t-att-href="'/download/jod/%s' % applicant.id" class="btn btn-primary">Download JOD</a>
</div>
</t>
</template>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -0,0 +1,563 @@
@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;
}
/* Base button styling */
.btn-stage {
position: relative;
min-width: 100px;
padding: 0.5rem 1rem;
margin: 0 2px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
text-align: center;
transition: all 0.25s ease;
cursor: pointer;
border: 1px solid #ddd;
outline: none;
}
/* Current stage styling */
.btn-stage-current {
background-color: #0d6efd; /* Primary blue */
color: white;
border-color: #0d6efd;
box-shadow: 0 2px 5px rgba(13, 110, 253, 0.3);
font-weight: 600;
}
/* Other stage options */
.btn-stage-option {
background-color: white;
color: #555;
border-color: #ddd;
}
/* Hover effects */
.btn-stage-option:hover {
background-color: #f0f7ff; /* Very light blue */
border-color: #0d6efd;
color: #0d6efd;
}
.btn-stage-current:hover {
background-color: #0b5ed7; /* Slightly darker blue */
}
/* Active state */
.btn-stage:active {
transform: translateY(1px);
}
/* Focus state */
.btn-stage:focus {
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
/* Disabled state during loading */
.btn-stage.processing {
opacity: 0.7;
pointer-events: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.btn-stage {
min-width: 80px;
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
margin: 2px;
}
}

View File

@ -0,0 +1,483 @@
/* ===== Modern Color Palette ===== */
:root {
/* Light Theme */
--primary-color: #4361ee;
--primary-light: #f0f4ff;
--primary-dark: #3a56d5;
--secondary-color: #6c757d;
--accent-color: #7209b7;
--bg-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
}
[data-theme="dark"] {
/* Dark Theme */
--primary-color: #5a7cff;
--primary-light: #1e293b;
--primary-dark: #4361ee;
--secondary-color: #94a3b8;
--accent-color: #9d4edd;
--bg-color: #0f172a;
--surface-color: #1e293b;
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--border-color: #334155;
}
/* Add smooth transitions for theme switching */
body.ats-app {
transition: background-color 0.3s ease, color 0.3s ease;
}
/* ===== Theme Toggle Styles ===== */
.theme-toggle-container {
padding: 1rem;
margin-top: auto; /* Push to bottom */
border-top: 1px solid var(--border-color);
}
.theme-toggle {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border-radius: 8px;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.theme-toggle:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.light-icon {
display: none;
color: #fbbf24; /* Amber color for sun */
}
.dark-icon {
color: #cbd5e1; /* Light gray for moon */
}
[data-theme="dark"] .light-icon {
display: block;
}
[data-theme="dark"] .dark-icon {
display: none;
}
.theme-text {
transition: opacity 0.2s ease;
}
/* Collapsed state styles */
.sidebar.collapsed .theme-text {
display: none;
}
.sidebar.collapsed .theme-toggle {
justify-content: center;
padding: 0.75rem 0;
}
.sidebar.collapsed .theme-toggle i {
margin-right: 0;
font-size: 1.25rem;
}
/* ===== Global Reset & Typography ===== */
body.ats-app {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
line-height: 1.5;
}
/* ===== Main Layout ===== */
.ats-app .layout-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* ===== Header (Modern Design) ===== */
.ats-app .main-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 2rem;
background-color: var(--surface-color);
box-shadow: var(--shadow-sm);
z-index: 10;
border-bottom: 1px solid var(--border-color);
}
.ats-app .main-header img {
height: 36px;
width: auto;
}
.ats-app .main-header span {
font-size: 1.25rem;
font-weight: 600;
color: var(--primary-color);
letter-spacing: -0.5px;
}
/* ===== Sidebar (Modern Redesign) ===== */
.ats-app .sidebar {
background-color: var(--surface-color);
color: var(--text-secondary);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 1.5rem 0;
position: relative;
border-right: 1px solid var(--border-color);
}
/* Sidebar States */
.ats-app .sidebar.expanded {
width: 280px;
min-width: 280px;
}
.ats-app .sidebar.collapsed {
width: 88px;
min-width: 88px;
padding: 1.5rem 0;
}
.ats-app .sidebar.collapsed .menu-item-text,
.ats-app .sidebar.collapsed .list-title,
.ats-app .sidebar.collapsed .main-header span {
opacity: 0;
width: 0;
height: 0;
overflow: hidden;
position: absolute;
}
/* ===== Navigation Menu (Improved UX) ===== */
.ats-app .menu-list {
list-style: none;
padding: 0;
margin: 1rem 0;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.ats-app .list-title {
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05rem;
margin: 1.5rem 1.5rem 0.5rem;
transition: all 0.3s ease;
}
.ats-app .menu-list a {
display: flex;
align-items: center;
gap: 1rem;
color: var(--text-secondary);
text-decoration: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius-md);
transition: all 0.2s ease;
font-size: 0.95rem;
margin: 0 0.5rem;
position: relative;
}
.ats-app .menu-list a i {
font-size: 1.25rem;
min-width: 24px;
text-align: center;
}
/* Hover and Active State */
.ats-app .menu-list a:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.ats-app .menu-list a.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.ats-app .menu-list a.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background-color: var(--primary-color);
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
/* Toggle Button (Improved) */
.ats-app .toggle-btn {
position: absolute;
top: 1.5rem;
right: -12px;
background-color: var(--surface-color);
color: var(--primary-color);
border: 1px solid var(--border-color);
padding: 0.5rem;
cursor: pointer;
border-radius: 50%;
box-shadow: var(--shadow-sm);
z-index: 20;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.3s ease;
}
.ats-app .toggle-btn:hover {
background-color: var(--primary-color);
color: white;
transform: translateX(2px);
}
.ats-app .sidebar.collapsed .toggle-btn {
right: -12px;
transform: rotate(180deg);
}
.ats-app .sidebar.collapsed .toggle-btn:hover {
transform: rotate(180deg) translateX(2px);
}
/* ===== Main Content Area (Improved) ===== */
.ats-app .content-area {
flex: 1;
padding: 3px;
background-color: var(--bg-color);
overflow-y: auto;
min-width: 0;
scroll-behavior: smooth;
}
/* Content header */
.ats-app .content-header {
margin-bottom: 1.5rem;
}
.ats-app .content-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 0.5rem;
}
/* ===== Responsive Design (Enhanced) ===== */
@media (max-width: 1024px) {
.ats-app .layout-container {
flex-direction: column;
}
.ats-app .sidebar {
width: 100%;
height: auto;
padding: 0;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.ats-app .sidebar.expanded {
height: auto;
}
.ats-app .sidebar.collapsed {
height: 60px;
overflow: hidden;
}
.ats-app .menu-list {
flex-direction: row;
flex-wrap: wrap;
margin: 0;
padding: 0.5rem;
gap: 0.25rem;
}
.ats-app .menu-list a {
flex-direction: column;
padding: 0.75rem;
gap: 0.25rem;
font-size: 0.75rem;
margin: 0;
}
.ats-app .menu-list a i {
font-size: 1.1rem;
}
.ats-app .menu-list a.active::before {
width: 100%;
height: 3px;
top: auto;
bottom: 0;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.ats-app .list-title {
display: none;
}
.ats-app .toggle-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
top: auto;
left: auto;
background-color: var(--primary-color);
color: white;
box-shadow: var(--shadow-lg);
width: 48px;
height: 48px;
font-size: 1.25rem;
}
.ats-app .content-area {
padding: 1.25rem;
}
}
/* Animation for smoother transitions */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.ats-app .content-area > * {
animation: fadeIn 0.3s ease-out;
}
/* Menu list styles */
.menu-list {
list-style: none;
padding: 0;
margin: 0;
}
.menu-list li {
margin-bottom: 4px;
}
.menu-item {
display: flex;
align-items: center;
padding: 12px 16px;
color: var(--text-secondary);
text-decoration: none;
border-radius: 6px;
transition: all 0.2s ease;
}
.menu-item:hover {
background-color: var(--primary-light);
color: var(--primary-color);
}
.menu-item.active {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.menu-icon {
font-size: 1.25rem;
min-width: 24px;
margin-right: 12px;
}
.menu-item-text {
transition: opacity 0.2s ease;
}
/* Collapsed state styles */
.sidebar.collapsed .menu-item {
justify-content: center;
padding: 12px 0;
}
.sidebar.collapsed .menu-icon {
margin-right: 0;
font-size: 1.4rem;
}
.sidebar.collapsed .menu-item-text {
display: none;
}
/* Active state indicator for collapsed */
.sidebar.collapsed .menu-item.active::after {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background-color: var(--primary-color);
border-radius: 0 4px 4px 0;
}
/* Mobile responsive styles */
@media (max-width: 768px) {
.sidebar.collapsed .menu-list {
display: flex;
justify-content: space-around;
}
.sidebar.collapsed .menu-item {
flex-direction: column;
padding: 8px 4px;
font-size: 0.75rem;
}
.sidebar.collapsed .menu-icon {
font-size: 1.2rem;
margin-bottom: 4px;
}
.sidebar.collapsed .menu-item.active::after {
left: 50%;
top: auto;
bottom: 0;
transform: translateX(-50%);
width: 24px;
height: 3px;
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

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

View File

@ -0,0 +1,229 @@
document.addEventListener("DOMContentLoaded", function () {
// Initialize application
initTheme();
initSidebar();
initNavigation();
// Load default page based on URL hash or default to jobs
});
/**
* Initialize theme functionality
*/
function initTheme() {
const themeToggle = document.getElementById('themeToggle');
const htmlElement = document.documentElement;
// Get saved theme or use system preference
const getPreferredTheme = () => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) return storedTheme;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
// Apply theme
const applyTheme = (theme) => {
htmlElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
updateThemeButton(theme);
};
// Update toggle button state
const updateThemeButton = (theme) => {
const themeText = theme === 'dark' ? 'Light Mode' : 'Dark Mode';
if (document.querySelector('.theme-text')) {
document.querySelector('.theme-text').textContent = themeText;
}
};
// Initialize with preferred theme
applyTheme(getPreferredTheme());
// Toggle theme on button click
if (themeToggle) {
themeToggle.addEventListener('click', () => {
const currentTheme = htmlElement.getAttribute('data-theme');
applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
});
}
document.querySelectorAll('.btn-stage').forEach(button => {
button.addEventListener('click', function() {
const container = this.closest('.stage-selector');
const buttons = container.querySelectorAll('.btn-stage');
const applicantId = container.dataset.applicantId;
const newStageId = this.dataset.stageId;
// Visual feedback
buttons.forEach(btn => {
btn.classList.remove('btn-stage-current');
btn.classList.add('btn-stage-option');
});
this.classList.remove('btn-stage-option');
this.classList.add('btn-stage-current', 'processing');
// AJAX call to update stage
fetch('/update_stage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCSRFToken()
},
body: JSON.stringify({
applicant_id: applicantId,
stage_id: newStageId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Stage updated successfully!', 'success');
} else {
throw new Error(data.error || 'Update failed');
}
})
.catch(error => {
console.error("Error:", error);
showNotification('Failed to update stage', 'danger');
// Revert visual state if needed
})
.finally(() => {
this.classList.remove('processing');
});
});
});
function getCSRFToken() {
return document.querySelector('meta[name="csrf-token"]').content;
}
function showNotification(message, type) {
// Your notification implementation
}
// Watch for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
}
/**
* Initialize sidebar functionality
*/
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebar-toggle-btn');
if (!sidebar || !toggleBtn) return;
// Toggle sidebar state
const toggleSidebar = () => {
sidebar.classList.toggle('collapsed');
sidebar.classList.toggle('expanded');
// Update toggle button icon
const icon = toggleBtn.querySelector('i');
icon.textContent = sidebar.classList.contains('collapsed') ? '>>' : '<<';
// Store preference
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
};
// Initialize sidebar state
const sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
if (sidebarCollapsed) {
sidebar.classList.add('collapsed');
sidebar.classList.remove('expanded');
toggleBtn.querySelector('i').textContent = '>>';
}
// Add click event
toggleBtn.addEventListener('click', toggleSidebar);
}
/**
* Initialize page navigation
*/
function initNavigation() {
// Highlight current menu item based on hash
const highlightActiveMenu = () => {
const hash = window.location.hash;
document.querySelectorAll('.menu-list a').forEach(link => {
link.classList.toggle('active', link.getAttribute('href') === hash);
});
};
// Handle page loading
const loadPage = async (hash) => {
if (!hash) return;
try {
const contentArea = document.getElementById('main-content');
const jobDetailArea = document.getElementById('job-detail');
const page = hash.substring(1); // Remove #
// Highlight active menu
highlightActiveMenu();
// Load page content
const res = await fetch(`/myATS/page/${page}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const html = await res.text();
contentArea.innerHTML = html;
if (jobDetailArea) jobDetailArea.innerHTML = "";
// Initialize page-specific JS
initPageScripts(page);
} catch (error) {
console.error('Error loading page:', error);
document.getElementById('main-content').innerHTML = `
<div class="alert alert-danger">
Error loading page: ${error.message}
</div>
`;
}
};
// Initialize page-specific scripts
const initPageScripts = (page) => {
switch (page) {
case 'jobs':
case 'job_requests':
if (typeof initJobListPage === 'function') initJobListPage();
break;
case 'applicants':
if (typeof initApplicantsPage === 'function') initApplicantsPage();
break;
case 'candidates':
if (typeof initCandidatesPage === 'function') initCandidatesPage();
break;
}
};
// Set up navigation events
document.querySelectorAll('.menu-list a[data-page]').forEach(link => {
link.addEventListener('click', async (e) => {
e.preventDefault();
const hash = link.getAttribute('href');
window.location.hash = hash;
await loadPage(hash);
});
});
// Handle hash changes
window.addEventListener('hashchange', () => {
loadPage(window.location.hash);
});
// Initial highlight
highlightActiveMenu();
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,89 @@
<?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=0.8"/>
<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=8"/>
<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="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"/>
<!-- <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=20"></script>
<script type="text/javascript" src="/hr_recruitment_web_app/static/src/js/applicants.js?v=10"></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>
</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 list-unstyled">
<!-- <span class="list-title">RECRUITING</span> -->
<li>
<a href="#jobs" data-page="job_requests" class="menu-item">
<i class="bi bi-file-earmark-text menu-icon"></i>
<span class="menu-item-text">JD</span>
</a>
</li>
<li>
<a href="#applicants" data-page="applicants" class="menu-item">
<i class="bi bi-people menu-icon"></i>
<span class="menu-item-text">Applicants</span>
</a>
</li>
<li>
<a href="#candidates" data-page="candidates" class="menu-item">
<i class="bi bi-person-check menu-icon"></i>
<span class="menu-item-text">Candidates</span>
</a>
</li>
</ul>
<div class="theme-toggle-container">
<button id="themeToggle" class="theme-toggle">
<i class="bi bi-moon-fill dark-icon"></i>
<i class="bi bi-sun-fill light-icon"></i>
<span class="theme-text">Dark Mode</span>
</button>
</div>
<button id="sidebar-toggle-btn" class="btn btn-outline-secondary position-absolute top-0 start-100 translate-middle-y mt-3">
<i>&lt;&lt;</i>
</button>
</aside>
<main class="content-area">
<div id="main-content"></div>
<div id="job-detail"></div>
</main>
</div>
</body>
</html>
</t>
</template>
</odoo>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
{
'name': 'Menu Access Control',
'version': '1.0',
'summary': 'Control menu visibility based on users or companies',
'description': """
This module allows administrators to configure menu visibility
based on selected users or companies. Active parent menus can
be generated and assigned for access control.
""",
'category': 'Tools',
'author': 'PRANAY',
'website': 'https://ftprotech.in',
'depends': ['base','hr'],
'data': [
'security/ir.model.access.csv',
'data/data.xml',
'views/menu_access_control_views.xml',
],
# 'assets': {
# 'web.assets_backend': [
# 'menu_control_center/static/src/js/menu_service.js',
# ],
# },
'installable': True,
'application': True,
'auto_install': False,
'license': 'LGPL-3',
}

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record id="hr_unit_menu_control" model="menu.control.units">
<field name="unit_name">Human Resources</field>
</record>
<record id="admin_unit_menu_control" model="menu.control.units">
<field name="unit_name">Administration</field>
</record>
<record id="developer_unit_menu_control" model="menu.control.units">
<field name="unit_name">Development</field>
</record>
<record id="testing_unit_menu_control" model="menu.control.units">
<field name="unit_name">Quality Assurance</field>
</record>
<record id="it_support_menu_control" model="menu.control.units">
<field name="unit_name">IT Support</field>
</record>
<record id="finance_unit_menu_control" model="menu.control.units">
<field name="unit_name">FINANCE</field>
</record>
</data>
</odoo>

View File

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

View File

@ -1,67 +0,0 @@
from odoo import models, fields, api, tools, _
from collections import defaultdict
class IrUiMenu(models.Model):
_inherit = 'ir.ui.menu'
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
def _visible_menu_ids(self, debug=False):
""" Return the ids of the menu items visible to the user. """
# retrieve all menus, and determine which ones are visible
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo()
# first discard all menus with groups the user does not have
group_ids = set(self.env.user._get_group_ids())
if not debug:
hide_menus_list = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids
menus = menus.filtered(lambda menu: (menu.id not in hide_menus_list))
group_ids = group_ids - {
self.env['ir.model.data']._xmlid_to_res_id('base.group_no_one', raise_if_not_found=False)}
menus = menus.filtered(
lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids)))
# take apart menus that have an action
actions_by_model = defaultdict(set)
for action in menus.mapped('action'):
if action:
actions_by_model[action._name].add(action.id)
existing_actions = {
action
for model_name, action_ids in actions_by_model.items()
for action in self.env[model_name].browse(action_ids).exists()
}
action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions)
folder_menus = menus - action_menus
visible = self.browse()
# process action menus, check whether their action is allowed
access = self.env['ir.model.access']
MODEL_BY_TYPE = {
'ir.actions.act_window': 'res_model',
'ir.actions.report': 'model',
'ir.actions.server': 'model_name',
}
# performance trick: determine the ids to prefetch by type
prefetch_ids = defaultdict(list)
for action in action_menus.mapped('action'):
prefetch_ids[action._name].append(action.id)
for menu in action_menus:
action = menu.action
action = action.with_prefetch(prefetch_ids[action._name])
model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[action._name]]
if not model_name or access.check(model_name, 'read', False):
# make menu visible, and its folder ancestors, too
visible += menu
menu = menu.parent_id
while menu and menu in folder_menus and menu not in visible:
visible += menu
menu = menu.parent_id
return set(visible.ids)

View File

@ -1,65 +0,0 @@
# models/models.py
from odoo import models, fields, api, tools, _
from collections import defaultdict
class MenuControlUnits(models.Model):
_name = 'menu.control.units'
_rec_name = 'unit_name'
_sql_constraints = [
('unique_unit_name', 'UNIQUE(unit_name)', "'Unit Name' already defined. Please don't confuse me 😤.")
]
unit_name = fields.Char(string='Unit Name',required=True)
department_ids = fields.Many2many('hr.department')
user_ids = fields.Many2many('res.users')
def generate_department_user_ids(self):
for rec in self:
user_ids = self.env['hr.employee'].sudo().search([('department_id','in',rec.department_ids.ids)]).user_id.ids
self.write({
'user_ids': [(6, 0, user_ids)]
})
class MenuAccessControl(models.Model):
_name = 'menu.access.control'
_description = 'Menu Access Control'
_rec_name = 'control_unit'
_sql_constraints = [
('unique_control_unit', 'UNIQUE(control_unit)', "Only one service can exist with a specific control_unit. Please don't confuse me 🤪.")
]
control_unit = fields.Many2one('menu.control.units',required=True)
user_ids = fields.Many2many('res.users', string="Users", related='control_unit.user_ids')
access_menu_line_ids = fields.One2many(
'menu.access.line', 'access_control_id',
string="Accessible Menus"
)
def action_generate_menus(self):
"""Button to fetch active top-level menus and populate access lines."""
menu_lines = []
active_menus = self.env['ir.ui.menu'].search([
('parent_id', '=', False), # top-level menus
('active', '=', True)
])
for menu in active_menus:
menu_lines.append((0, 0, {
'menu_id': menu.id,
'is_main_menu': True
}))
self.access_menu_line_ids = menu_lines
class MenuAccessLine(models.Model):
_name = 'menu.access.line'
_description = 'Menu Access Line'
_rec_name = 'menu_id'
access_control_id = fields.Many2one('menu.access.control', ondelete='cascade')
menu_id = fields.Many2one('ir.ui.menu', string="Menu")
is_main_menu = fields.Boolean(string="Is Main Menu", default=True)

View File

@ -1,4 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_menu_access_control,access.menu.access.control,model_menu_access_control,hr.group_hr_manager,1,1,1,1
access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_hr_manager,1,1,1,1
access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_menu_access_control access.menu.access.control model_menu_access_control hr.group_hr_manager 1 1 1 1
3 access_menu_access_line access.menu.access.line model_menu_access_line hr.group_hr_manager 1 1 1 1
4 access_menu_control_units access.menu.control.units model_menu_control_units hr.group_hr_manager 1 1 1 1

View File

@ -1,44 +0,0 @@
///** @odoo-module **/
//
//import { browser } from "@web/core/browser/browser";
//import { makeEnv, startServices } from "@web/env";
//import { session } from "@web/session";
//import { _t } from "@web/core/l10n/translation";
//import { browser } from "@web/core/browser/browser";
//import { MenuService } from "@web/services/menu_service";
//
//export const menuService = {
// dependencies: MenuService.dependencies,
// start(env, deps) {
// const menu = super.start(env, deps);
//
// // Override the getApps method
// const originalGetApps = menu.getApps;
// menu.getApps = async function() {
// // Get the original apps
// let apps = await originalGetApps.call(this);
//
// // Fetch your custom menu access data
// const accessData = await this.orm.call(
// 'menu.access.control',
// 'get_user_access_menus',
// []
// );
//
// // If access data exists, filter the apps
// if (accessData && accessData.allowed_menu_ids) {
// apps = apps.filter(app =>
// accessData.allowed_menu_ids.includes(app.id)
// );
// }
//
// return apps;
// };
//
// return menu;
// },
//};
//
//// Register the service
//import { registry } from "@web/core/registry";
//registry.category("services").add("menu", menuService, { force: true });

View File

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_menu_control_units_list" model="ir.ui.view">
<field name="name">menu.control.units.list</field>
<field name="model">menu.control.units</field>
<field name="arch" type="xml">
<list>
<field name="unit_name"/>
<field name="user_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<record id="view_menu_control_units_form" model="ir.ui.view">
<field name="name">menu.control.units.form</field>
<field name="model">menu.control.units</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="unit_name"/>
<field name="department_ids" widget="many2many_tags"/>
<button name="generate_department_user_ids" string="Generate Department Users" type="object" class="btn-primary"/>
</group>
<notebook>
<page string="User's">
<field name="user_ids"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_menu_control_units" model="ir.actions.act_window">
<field name="name">Menu Control Units</field>
<field name="res_model">menu.control.units</field>
<field name="view_mode">list,form</field>
</record>
<record id="view_menu_access_control_list" model="ir.ui.view">
<field name="name">menu.access.control.list</field>
<field name="model">menu.access.control</field>
<field name="arch" type="xml">
<list>
<field name="control_unit"/>
<field name="user_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<record id="view_menu_access_control_form" model="ir.ui.view">
<field name="name">menu.access.control.form</field>
<field name="model">menu.access.control</field>
<field name="arch" type="xml">
<form string="Menu Access Control">
<sheet>
<group>
<field name="control_unit"/>
<field name="user_ids" widget="many2many_tags"/>
<button name="action_generate_menus" type="object" string="Generate Menus" class="btn-primary"/>
</group>
<field name="access_menu_line_ids">
<list editable="bottom">
<field name="menu_id"/>
<field name="is_main_menu"/>
</list>
</field>
</sheet>
</form>
</field>
</record>
<record id="action_menu_access_control" model="ir.actions.act_window">
<field name="name">Menu Access Control</field>
<field name="res_model">menu.access.control</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_menu_access_root"
name="Menu Access Control"
parent="base.menu_custom"
sequence="10"/>
<menuitem id="menu_menu_control_units"
name="Control Units Master"
parent="menu_menu_access_root"
action="action_menu_control_units"
sequence="19"/>
<menuitem id="menu_menu_access_control"
name="Access Control"
parent="menu_menu_access_root"
action="action_menu_access_control"
sequence="20"/>
</odoo>

View File

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

View File

@ -1,30 +0,0 @@
{
'name': 'Offer Letters',
'version': '1.0.0',
'summary': 'Generate and manage employee offer letters',
'description': """
This module allows HR to create, manage and send offer letters to candidates
with a modern React.js interface for enhanced user experience.
""",
'author': 'Raman Marikanti',
'category': 'Human Resources',
'depends': ['base', 'hr_recruitment','hr_payroll','hr_ftp'],
'data': [
'security/ir.model.access.csv',
'views/offer_letter_views.xml',
# 'views/templates.xml',
'views/menu_views.xml',
'report/offer_letter_report.xml',
'report/offer_letter_template.xml',
],
'assets': {
'web.assets_backend': [
'offer_letters/static/src/js/pay_details_widget.js',
],
},
'demo': [],
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

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

View File

@ -1,225 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from collections import defaultdict
from datetime import timedelta, datetime
from odoo.tools.safe_eval import safe_eval
import json
import calendar
class DefaultDictroll(defaultdict):
def get(self, key, default=None):
if key not in self and default is not None:
self[key] = default
return self[key]
class OfferLetter(models.Model):
_name = 'offer.letter'
_description = 'Employee Offer Letter'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
name = fields.Char(
string='Reference',
required=True,
default=lambda self: _('New'),
copy=False
)
candidate_id = fields.Many2one( 'hr.applicant', string='Candidate', required=True,
)
employee_id = fields.Char(
string='Employee ID',
readonly=True
)
position = fields.Char(
string='Position',
required=True
)
salary = fields.Float(
string='Salary',
required=True
)
mi = fields.Float(
string='Medical Insurance',
)
currency_id = fields.Many2one(
'res.currency',
string='Currency',
default=lambda self: self.env.company.currency_id
)
joining_date = fields.Date(
string='Joining Date',
default=lambda self: (datetime.now() + timedelta(days=14)).strftime('%Y-%m-%d'))
contract_type = fields.Selection([
('permanent', 'Permanent'),
('contract', 'Fixed Term Contract'),
('intern', 'Internship')],
string='Contract Type',
default='permanent'
)
probation_period = fields.Integer(
string='Probation Period (months)',
default=3
)
terms_conditions = fields.Text(
string='Terms and Conditions',
default=lambda self: self._default_terms()
)
state = fields.Selection([
('draft', 'Draft'),
('sent', 'Sent'),
('accepted', 'Accepted'),
('rejected', 'Rejected'),
('expired', 'Expired')],
string='Status',
default='draft',
tracking=True
)
sent_date = fields.Datetime(string='Sent Date')
response_date = fields.Datetime(string='Response Date')
pay_struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
manager_id = fields.Many2one('hr.employee', string='Manager')
@api.model
def _default_terms(self):
return """
<p>1. This offer is contingent upon satisfactory reference checks.</p>
<p>2. You will be required to sign a confidentiality agreement.</p>
<p>3. The company reserves the right to modify job responsibilities.</p>
"""
@api.model
def create(self, vals):
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
return super(OfferLetter, self).create(vals)
def action_send_offer(self):
self.ensure_one()
# template = self.env.ref('offer_letters.email_template_offer_letter')
self.write({'state': 'sent', 'sent_date': fields.Datetime.now()})
# template.send_mail(self.id, force_send=True)
return True
def action_accept_offer(self):
self.ensure_one()
# employee = self.env['hr.employee'].create({
# 'name': self.candidate_id.partner_name,
# 'job_title': self.position,
# 'department_id': self.department_id.id,
# 'currency_id': self.currency_id.id,
# })
self.write({
'state': 'accepted',
# 'employee_id': employee,
'response_date': fields.Datetime.now()
})
return True
def action_reject_offer(self):
self.ensure_one()
self.write({
'state': 'rejected',
'response_date': fields.Datetime.now()
})
return True
@api.onchange('candidate_id')
def _onchange_candidate_id(self):
self.position = self.candidate_id.job_id.name
def get_paydetailed_lines(self):
today = fields.Date.today()
first_day = today.replace(day=1)
last_day = today.replace(day=calendar.monthrange(today.year, today.month)[1])
payslip = self.env['hr.payslip'].new({
'date_from': first_day,
'date_to': last_day,
})
contract = self.env['hr.contract'].new({
'date_start': first_day,
'date_end': last_day,
'l10n_in_medical_insurance':self.mi,
'l10n_in_provident_fund': True,
'name': 'test',
'wage': self.salary / 12,
})
categories_dict = {}
rules_dict = {}
result = {}
localdict = {
'payslip': payslip,
'contract': contract,
'worked_days': {},
'categories': defaultdict(lambda: 0), # Fixed: Changed DefaultDictroll to defaultdict
'rules': defaultdict(lambda: dict(total=0, amount=0, quantity=0)),
'result': None,
'result_qty': 1.0,
'result_rate': 100,
'result_name': False,
'inputs': {},
}
blacklisted_ids = set(self.env.context.get('prevent_payslip_computation_line_ids', []))
for rule in sorted(self.pay_struct_id.rule_ids, key=lambda r: r.sequence):
if rule.id in blacklisted_ids or not rule._satisfy_condition(localdict):
continue
qty = 1.0
rate = 100.0
amount = 0.0
try:
if rule.amount_select == 'fix':
amount = rule.amount_fix
elif rule.amount_select == 'percentage':
base = float(safe_eval(rule.amount_percentage_base or '0.0', localdict, mode='exec', nocopy=True))
amount = base * rule.amount_percentage / 100
elif rule.amount_select == 'code':
safe_eval(rule.amount_python_compute or '0.0', localdict, mode='exec', nocopy=True)
amount = float(localdict.get('result', 0.0))
except Exception as e:
raise UserError(_("Error in rule %s: %s") % (rule.name, str(e)))
total = payslip._get_payslip_line_total(amount, qty, rate, rule)
rule_code = rule.code
previous_amount = localdict.get(rule.code, 0.0)
category_code = rule.category_id.code
tot_rule = payslip._get_payslip_line_total(amount, qty, rate, rule)
# Make sure _sum_salary_rule_category method exists
if hasattr(rule.category_id, '_sum_salary_rule_category'):
localdict = rule.category_id._sum_salary_rule_category(localdict, tot_rule - previous_amount)
localdict[rule_code] = total
rules_dict[rule_code] = rule
categories_dict[category_code] = categories_dict.get(category_code, 0.0) + amount
result[rule_code] = {
'sequence': rule.sequence,
'code': rule_code,
'name': rule.name, # Simplified name retrieval
'salary_rule_id': rule.id,
'amount': round(amount,2),
'y_amount': round((amount * 12),2),
'quantity': qty,
'rate': rate,
'total': round(total,2),
}
self.terms_conditions = json.dumps(list(result.values()))
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'view_type': 'form',
'target': 'current',
}
def generate_pdf_report(self):
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(self)

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_offer_letters_employee_print" model="ir.actions.report">
<field name="name">Offer Letter</field>
<field name="model">offer.letter</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">offer_letters.report_offer_letter</field>
<field name="report_file">offer_letters.report_offer_letter</field>
<field name="print_report_name">'Offer Letter - %s' % (object.name).replace('/', '')</field>
<field name="binding_model_id" ref="model_offer_letter"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@ -1,396 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="external_layout_offer_letter">
<!-- support for custom header -->
<t t-set="company" t-value="env.company"/>
<div t-attf-class="header pt-5">
<span class="row">
<img class="col-2" t-att-src="'/logo.png?company=%s' % company.id"
style="padding: 0px; margin:0px; height: 100px; width: 100px;vertical-align: top"
t-att-alt="'%s' % company.name"/>
<h3 class="col-10"
style="font-weight: bold;vertical-align: bottom;padding-top: 15px;font-family: 'Times New Roman', Times, serif; text-align:center">
FAST TRACK PROJECTS PVT LTD.
</h3>
</span>
</div>
<main>
<t t-out="0"/>
</main>
<div t-attf-class="footer" style="padding-top:20px;font-size:14px">
<p style="text-align:center">
<strong>
<span style="font-weight: bold" t-esc="env.company.name"/> - Unit of FAST TRACK PROJECTS PVT LTD.,
<br/>
Unit 302, Surya Arcade, Cyber Hills, Gachibowli, Hyderabad, 500032 Telangana INDIA
<br/>
Phone: +91 40 42601144 www.ftprotech.com | CIN: U45201TG2000PTC035652
</strong>
</p>
</div>
</template>
<template id="report_offer_letter">
<t t-foreach="docs" t-as="o">
<t t-call="offer_letters.external_layout_offer_letter">
<div class="page p-10" width="100%" height="100%">
<table class="table" style="line-height:20px; font-size:14px;" width="100%">
<tr width="100%">
<td style="text-decoration: underline;padding-left:10px;color:blue;padding-top:15px">PRIVATE &amp; CONFIDENTIAL</td>
</tr>
<tr>
<td style="font-weight: bold"><t t-esc="o.name"/></td>
</tr>
<tr>
<td style="font-weight: bold">To,</td>
</tr>
<tr>
<td><span style="font-weight: bold" t-esc="o.candidate_id.partner_name"/><br/>
<div id="employee_private_address" t-if="o.candidate_id.private_street and o.candidate_id.private_city">
<span t-field="o.candidate_id.private_street"/>,<br/>
<span t-field="o.candidate_id.private_street2" t-if="o.candidate_id.private_street2" class="ms-2"/><span t-if="o.candidate_id.private_street2">,</span>
<br/> <span t-field="o.candidate_id.private_city" class="ms-2"/>
<span t-field="o.candidate_id.private_zip"/>
<span t-field="o.candidate_id.private_country_id"/>
</div>
<br/><br/></td>
</tr>
<tr>
<td style="font-weight: bold;">Dear <span t-esc="o.candidate_id.partner_name"></span>,</td>
</tr>
<tr>
<td>We are pleased to extend you an offer to join <span style="font-weight: bold" t-esc="env.company.name"/> as a <span style="font-weight: bold" t-esc="o.position"/><br/>
This letter will memorialize the terms of your employment by <span style="font-weight: bold" t-esc="env.company.name"/> Your employment is contingent on your ability to obtain employment eligibility documentation as required by law. However, if considered expedient and necessary, we may conduct background checks on you on our own or through a third party. You hereby consent to any such background checks and undertake to co-operate if so requested by us.
We are excited and looking forward to your joining.<br/>The terms of your employment are as follows:<br/><br/>Start Date: You will have to report on or before <span style="font-weight: bold" t-esc="o.joining_date"/>
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">1. Place Of Work: </span>Your assigned work location is Hyderabad. The Company may, after giving you reasonable notice, transfer or assign your services to any place of business of the Company that may presently be operating, or which may subsequently be acquired or established, in any part of India or abroad.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">2. Leave and Working Hours: </span>You will be entitled to leave as per company policy and will observe the working hours as may be applicable to your category of employees and location of posting.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">3. Fulfillment Obligation: </span>Any cash bonuses or other expenses paid prior to normal salary periods are recoverable by the Company for the first 90 days of employment should you terminate your employment without cause.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">4. Confidentiality and Invention Assignment: </span>Your employment is conditioned upon your execution of Confidentiality and Invention Assignment Agreements and agreement to abide by the terms and conditions of those Agreements. Failure to abide by the terms of the Agreements may result in your dismissal, and you are subject to their terms even after the termination of your employment.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">5. Transfer &amp; Relocation: </span>You will be liable to transfer in such capacity as the company may from time to time determine to any other location, department, establishment, factory or branch of the company or its affiliate, associate or subsidiary companies. In such case, you will be governed by the terms and conditions of service applicable to the new assignment.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">6. Non-Compete: </span>You agree that during the term of your employment and for further period of 6 calendar months after separation from the Company, for whatever reasons, you shall not carry on or engage in directly or indirectly in any business which competes directly or indirectly with any or all the business pursued by the Company in any territory, whether in India or overseas, at the relevant point of time or proposed to be pursued by the Company in the immediate future, in respect of which proposal you were aware of or likely to be aware of considering the nature of your duties , other than through the Company.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">7. Non-Solicitation and Non Hire of Company Employees: </span>You agree that during the term of your employment and a further period of 24 (twenty four) calendar months after separation from the Company, for whatever reasons, you shall not either directly or indirectly solicit or entice away or endeavor to solicit or to entice away or assist any other Person to solicit or hire or entice away from the Company, any Company employee.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold;">8. Probation, Confirmation &amp; Termination: </span>
<br/> <span style="font-weight: bold;"> a) </span>You will be on probation for a period of180 Days from the date of your appointment, where after, post completion of 180 Days your service with the organization stands confirmed unless otherwise notified in writing through the HR team. The Company reserves the right to reduce or extend your probation period at its absolute discretion.
<br/> <span style="font-weight: bold;"> b) </span>During the probation period/ extended period of probation or, <span style="font-weight: bold">company</span> shall be entitled to terminate your employment without cause at any time by giving <span style="font-weight: bold">you 30 calendar days' notice or salary in lieu thereof</span>. However, in case of cause, the Company can terminate your employment immediately. If you wish to terminate your employment with the Company during the probation period/ extended period of probation then you shall be required to serve 90 calendar days' notice period from the day next to resignation or salary in lieu thereof. During the notice period you shall not be entitled to any paid or unpaid leave and the notice period cannot be adjusted by any accrued leave. The decision to waive the notice period lies at the sole discretion of the Company.
<br/> <span style="font-weight: bold;"> c) </span>No Leaves can be availed during your probation period expect for sick leave maximum of one per month.
<br/> <span style="font-weight: bold;"> d) </span>Absence for a continuous period of six working days without prior approval of your superior, can lead to your services being terminated without notice or explanation.
<br/> <span style="font-weight: bold;"> e) </span>No leave can be available during the notice period. Failure to complete the notice period will be considered equivalent to two times the shortfall in pay and the same will be adjusted as part of the full and final settlement.
<br/> <span style="font-weight: bold;"> f) </span>Post probation confirmation, the <span style="font-weight: bold">company</span> shall be entitled to <span style="font-weight: bold">terminate your employment</span>, without cause, at any time by giving out <span style="font-weight: bold">30 days' notice </span> or salary in lieu thereof. You are also bound to provide the company with <span style="font-weight: bold">90 days' notice period </span> from the day next to resignation or salary in lieu thereof. During the notice period you shall not be entitled to any paid or unpaid leave and the notice period cannot be adjusted by any accrued leave. The decision to waive the notice period lies at the sole discretion of the Company.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">9. Confidentiality &amp; Non-Complete and Non-Solicitation: </span>You certify not to share your salary or any company details along with not joining any competitor as an employee or contractor or solicit any employee from the company to join a company.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">10. Term: </span>The Company may terminate your employment for cause with immediate effect at any time. No salary or allowances will be paid for any period if you are terminated for cause.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">11. Salary: </span>Compensation of INR <span style="font-weight: bold"> <t t-esc="o.salary"/>/- p.a. only ( <t t-esc="env.company.currency_id.amount_to_text(o.salary)"/>). </span>.Refer to Annexure B
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">12. "Employee Agreement": </span>To protect the interests of the Company, you will need to sign the Company's standard Terms &amp; Conditions of Employment (attached as annexure A), invention assignment agreement, <span style="font-weight: bold" t-esc="env.company.name"/>, Confidentiality Agreement and conflict of interest agreement (collectively, the "Employee Agreements") as a condition of your employment. You represent that your signing of this offer letter, and the Employee Agreements and your commencement of employment with the Company will not breach any agreement currently in place between yourself and current or past employers.
</td>
</tr>
<tr>
<td width="100%">
Please confirm that this letter sets forth the terms of your employment with the Company by countersigning a copy of this letter below. Your signature below indicates that you fully understand the terms of your employment with the Company and that you enter this Agreement knowingly and of your own accord.
</td>
</tr>
<tr>
<td>
<br/>
<span style="font-weight: bold">Sincerely</span>
<br/>
</td>
</tr>
<tr>
<td style="width:100%">
For<br/>
Authorized Signatory
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold" t-esc="env.company.name"/>
<span style="white-space: nowrap;float: right;" >Accepted and Agreed
<br/><br/><br/><br/>
</span>
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold"><t t-esc="o.manager_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.candidate_id.partner_name"></span><br/>
<span style="font-weight: bold"><t t-esc="o.manager_id.job_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.position"></span><br/>
<span style="font-weight: bold" t-esc="o.joining_date"/><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.joining_date"/>
</td>
</tr>
</table>
<table class="table" style="line-height:20px; font-size:14px;page-break-before: always !important;" width="100%">
<tr>
<td style="text-align:center;"> <span style="font-weight: bold;">Annexure - A</span><br/><br/><span style="font-weight: bold;">Terms &amp; Conditions of Employment</span> </td>
</tr>
<tr>
<td>
<br/>
<span style="font-weight: bold">1. </span>During the term of your employment with <span style="font-weight: bold" t-esc="env.company.name"/> , you may not engage in any employment or act in any way, which either conflicts with your duties and obligations to <span style="font-weight: bold" t-esc="env.company.name"/>, or is contrary to the policies or the interests of <span style="font-weight: bold" t-esc="env.company.name"/>.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">2. </span>During the term of your employment with <span style="font-weight: bold" t-esc="env.company.name"/> you are required to disclose all material and relevant information, which may either affect your employment with <span style="font-weight: bold" t-esc="env.company.name"/> currently or in the future or may conflict with the terms of your employment with <span style="font-weight: bold" t-esc="env.company.name"/> either directly or indirectly. If at any time during your employment, if <span style="font-weight: bold" t-esc="env.company.name"/>. becomes aware that you have suppressed any material or relevant information required to be disclosed by you, <span style="font-weight: bold" t-esc="env.company.name"/> reserves the right to forthwith terminate your employment without any notice and without any obligation or liability to pay any remuneration or other dues to you irrespective of the period that you may have been employed by <span style="font-weight: bold" t-esc="env.company.name"/>.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">3. </span>You agree to conform to and comply with <span style="font-weight: bold" t-esc="env.company.name"/>s Policy and such other directions and guidelines which <span style="font-weight: bold" t-esc="env.company.name"/> may from time to time give as per its own discretion.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">4. </span>Notwithstanding anything mentioned in this Agreement, <span style="font-weight: bold" t-esc="env.company.name"/> may terminate your employment, with immediate effect by a notice in writing (without salary in lieu of notice), in the event of your misconduct, including but not limited to, fraudulent, dishonest or undisciplined conduct of, or breach of integrity, or embezzlement, or misappropriation or misuse by you of <span style="font-weight: bold" t-esc="env.company.name"/> s property, or insubordination or failure to comply with the directions given to you by persons so authorized, or your insolvency or conviction for any offence involving moral turpitude, or breach by you of any terms of this Agreement or <span style="font-weight: bold" t-esc="env.company.name"/> Policy or other documents or directions of <span style="font-weight: bold" t-esc="env.company.name"/> , or irregularity in attendance, or your unauthorized absence of from the place of work for more than Three (3) working days, or closure of the business of <span style="font-weight: bold" t-esc="env.company.name"/> , or redundancy of your post in <span style="font-weight: bold" t-esc="env.company.name"/>, or upon you conducting yourself in a manner which is regarded by <span style="font-weight: bold" t-esc="env.company.name"/> as prejudicial to its own interests or to the interests of its clients.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">5. </span>Notwithstanding anything aforesaid, termination by you shall be subject to the satisfactory completion of all your existing duties, obligations and projects.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">6. </span>At the time of termination of your employment, if there are any dues from you, the same may be adjusted against any money due to you from <span style="font-weight: bold" t-esc="env.company.name"/> on account of salary, bonus or any other such payments.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">7. </span>You agree that the laws of India shall govern the interpretation and enforcement of this Agreement and the provisions of the Indian Arbitration and Conciliation Act, 1996, shall govern all disputes under this Agreement. The venue for arbitration will be Hyderabad.
</td>
</tr>
<tr>
<td>
This is to certify that I have read this Agreement and understood all the terms and conditions mentioned therein and I hereby accept and agree to abide by them.
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold">Employee Name : <t t-esc="o.candidate_id.partner_name"/><br/><br/><br/><br/><br/>Signature: <br/>Date of Joining: <t t-esc="o.joining_date"/></span>
</td>
</tr>
</table>
<table class="table" style="line-height:20px; font-size:14px;page-break-before: always !important;" width="100%">
<tr>
<td style="text-align:center;" > <span style="font-weight: bold;">Annexure - B</span>
<br/><br/><span style="font-weight: bold;">Salary Break Up</span>
</td>
</tr>
<tr>
<td > <br/>
<t t-if="o.terms_conditions">
<style>
.custom-table-container {
width: 700px;
margin: 0 auto;
border-collapse: collapse;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 14px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border-radius: 8px;
overflow: hidden;
}
.custom-table-container th,
.custom-table-container td {
padding: 12px 15px;
border-bottom: 1px solid #e0e0e0;
text-align: left;
}
.custom-table-container thead {
background-color: #f5f5f5;
font-weight: bold;
color: #333;
}
.custom-table-container th.text-end,
.custom-table-container td.text-end {
text-align: right;
}
.custom-table-container tr.text-danger {
color: #d9534f;
background-color: #fff5f5;
}
.custom-table-container tbody tr:hover {
background-color: #f9f9f9;
}
</style>
<table class="custom-table-container">
<thead>
<tr>
<th>Sequence</th>
<!-- <th width="25%">Code</th>-->
<th width="50%">Name</th>
<th class="text-end" width="25%">Amount(M)</th>
<th class="text-end" width="25%">Amount(Y)</th>
</tr>
</thead>
<tbody>
<t t-set="pay_details_lines" t-value="json.loads(o.terms_conditions)"/>
<t t-set="n" t-value="0"/>
<t t-foreach="pay_details_lines" t-as="line">
<t t-set="n" t-value="n+1"/>
<tr t-att-class="'text-danger' if line.get('amount', 0) &lt; 0 else ''">
<td><t t-esc="n"/></td>
<!-- <td><t t-esc="line.get('code', '')"/></td>-->
<td><t t-esc="line.get('name', '')"/></td>
<td class="text-end"><t t-esc="'%.2f' % line.get('amount', 0)"/></td>
<td class="text-end"><t t-esc="'%.2f' % line.get('y_amount', 0)"/></td>
</tr>
</t>
</tbody>
</table>
</t>
</td>
<br/>
</tr>
<tr>
<td>
<br/>
<span style="font-weight: bold">1. ##VPP as per Policy.<br/>2. *Subject to Monthly Attendance and Subject to Income Tax Deductions<br/>3. **As per Statutory Norms<br/>4. Medical Insurance is for SELF only for coverage INR of 500000</span><br/><br/><br/>
</td>
</tr>
<tr>
<td>
<br/>
<span style="font-weight: bold">Sincerely</span>
<br/>
</td>
</tr>
<tr>
<td style="width:100%">
For<br/>
Authorized Signatory
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold" t-esc="env.company.name"/>
<span style="white-space: nowrap;float: right;" >Accepted and Agreed
<br/><br/><br/><br/>
</span>
</td>
</tr>
<tr>
<td>
<span style="font-weight: bold"><t t-esc="o.manager_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.candidate_id.partner_name"></span><br/>
<span style="font-weight: bold"><t t-esc="o.manager_id.job_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.position"></span><br/>
<span style="font-weight: bold" t-esc="o.joining_date"/><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.joining_date"/>
<br/>
</td>
</tr>
</table>
<table class="table" style="line-height:20px; font-size:14px;page-break-before: always !important;" width="100%">
<tr>
<td style="text-align:center;" > <span style="font-weight: bold;">Annexure - C</span><br/><br/><span style="font-weight: bold;">NONDISCLOSURE AGREEMENT</span> </td>
</tr>
<tr>
<td>
<span>THIS NONDISCLOSURE AGREEMENT (this "Agreement") is entered into and effective as of the date specified on the signature page below between <span t-esc="env.company.name"/> a Company incorporated under Indian Companies Act 1956, having registered office in Hyderabad, India ("Company") Ashwath Sreeram(Recipient)</span><br/>
<span>Whereas "Company" wishes to explore the possibility of entering into an employment agreement with Recipient which requires "Company" to disclose to Recipient certain Confidential Information as defined below.</span>
<span>Now therefore, in consideration of the rights and obligations contained herein, the parties agree as follows:</span>
</td>
</tr>
<tr>
<td>
<span>1. <span style="text-decoration: underline;padding-left:10px;">Confidential Information.</span> "Confidential Information" as used in this Agreement shall mean information in any form, disclosed by Company, which relates to its business. Confidential Information includes, but is not limited to, patents, employees and related information, trade secrets, research and development plans, current and future products, product pricing, customers lists, markets, business plans, financial data, contractual terms, documentation, records, studies, reports, know-how, test results, software, software source or object code, and any other information which reasonably ought to be considered to be Confidential Information.</span><br/>
<span>2. <span style="text-decoration: underline;padding-left:10px;">Exclusions.</span> Confidential Information does not include any information which (a) at the time of disclosure, is available to the general public; (b) at a later date, becomes available to the general public through no fault of Recipient, and then only after such later date; (c) Recipient can demonstrate was in its possession prior to receipt without an obligation of confidence; (d) is disclosed to Recipient without restriction on disclosure by a third party who had the lawful right to disclose such information; or (e) Recipient can demonstrate was independently developed by Recipient without use of any Confidential Information.</span><br/>
<span>3. <span style="text-decoration: underline;padding-left:10px;">Protection of Confidential Information.</span> Recipient agrees that it shall treat Confidential Information with the same degree of care as it accords to its own Confidential Information of like kind, but in no event less than a reasonable degree of care. Recipient agrees that it will not make use of, disseminate, or in any way disclose any Confidential Information to any person, firm or business, except to the extent necessary for negotiations, discussions, and consultations with personnel or authorized representatives of Company., and any other purpose which Company may hereafter authorize in writing. Recipient may disclose the Confidential Information pursuant to a valid court order provided that Company is given prompt notice of any such order and an opportunity to contest the order. Recipient agrees that it shall disclose Confidential Information only to those of its employees or consultants who have a legitimate business need to know such information and who have previously agreed, either as a condition of employment or to obtain the Confidential Information, to be bound by terms and conditions substantially similar to those in this Agreement.</span><br/>
<span>4. <span style="text-decoration: underline;padding-left:10px;">Return of Confidential Information.</span> All information furnished under this Agreement shall remain the property of the Company and shall be returned to it or destroyed or purged promptly at its request. All documents, memoranda, notes and other tangible embodiments whatsoever prepared by Recipient based on or which includes Confidential Information shall be destroyed to the extent necessary to remove all such Confidential Information upon the disclosing party's request. All destruction under this Paragraph 4 shall be certified in writing to the disclosing party by an authorized officer of Recipient.</span><br/>
<span>5. <span style="text-decoration: underline;padding-left:10px;">Export Regulations.</span> Recipient agrees that it shall not export the Confidential Information to any country to which export is restricted by the company.</span><br/>
<span>6. <span style="text-decoration: underline;padding-left:10px;">No License or Warranty.</span> Except as expressly set forth in this Agreement, no license under any patents, copyrights, mask rights or other proprietary rights is granted or conveyed by the Company's transmittal of Confidential Information or other information to Recipient under this Agreement. THE INFORMATION IS PROVIDED "AS IS" AND THERE IS NO REPRESENTATIONS OR
WARRANTIES, EXPRESS OR IMPLIED, WITH RESPECT TO THE INFORMATION, INCLUDING BUT NOT LIMITED TO A WARRANTY AGAINST INFRINGEMENT, ACCURACY OR
COMPLETENESS. Recipient will use all information received in a safe and prudent manner and is responsible for all risk or loss arising out of its use of such information. The recipient agrees that Company shall have no </span><br/>
<span>7. <span style="text-decoration: underline;padding-left:10px;">No Inducement or Commitment.</span> Confidential Information provided to Recipient does not and is not intended to represent an inducement by Company or a commitment by Company to enter into any business relationship with Recipient or with any other entity. If the parties desire to pursue business opportunities, the parties will execute a separate written agreement to govern such business relationship.</span><br/>
<span>8. <span style="text-decoration: underline;padding-left:10px;">Effective Date and Term.</span> This Agreement shall be effective from the Effective Date specified below and shall continue for three (3) years following the return of all Confidential Information in accordance with Paragraph 4 above when accompanied by a written notice of termination.</span><br/>
<span>9. <span style="text-decoration: underline;padding-left:10px;">Remedies.</span> It is understood and agreed that money damages would not be a sufficient remedy for any breach of this Agreement and that Company shall be entitled to seek injunctive relief as a remedy for any such breach. Such remedy shall not be deemed to be exclusive but shall be in addition to all other remedies available at law or equity.</span><br/>
<span>10. <span style="text-decoration: underline;padding-left:10px;">Non-assignment.</span> This Agreement may not be assigned, or otherwise transferred without the prior written consent of Company.</span><br/>
<span>11. <span style="text-decoration: underline;padding-left:10px;">Miscellaneous.</span> This Agreement embodies the entire understanding between the parties respecting the subject matter of this Agreement and supersedes any and all prior negotiations, correspondence, understandings and agreements between the parties respecting the use and disclosure of Confidential Information. This Agreement shall not be modified except by a writing duly executed on behalf of the party against whom such modification is sought to be enforced. The failure of any party to require performance by another party of any provision of this Agreement shall in no way affect the full right to require such performance at any time thereafter. Should any provisions of this Agreement be found unenforceable, the remainder shall still be in effect. This Agreement has been negotiated by the parties and their respective attorneys, and the language of this Agreement shall not be construed for or against either party. The headings are not part of this Agreement. Either the original or copies, including facsimile transmissions, of this Agreement, may be executed in counterparts, each of which shall be an original as against any party whose signature appears on such counterpart and all of which together shall constitute one and the same instrument.</span><br/>
<span>12. <span style="text-decoration: underline;padding-left:10px;">Notices.</span> All notices under this Agreement shall be deemed to have been duly given upon the mailing of the notice, post-paid, to the party entitled to such notice at the address set forth below.</span><br/>
<span>13. <span style="text-decoration: underline;padding-left:10px;">Governing Law.</span> This Agreement shall be interpreted under the laws of the State of Telangana, India</span><br/>
<span>14. All clauses mentioned in this NDA is applicable including our group Companies. Any misuse of data, information or any confidential information including our group companies will be prosecuted within Hyderabad Jurisdiction.</span><br/>
<span>IN WITNESS WHEREOF each of the parties has caused the Agreement to be executed by its duly authorized representative.</span><br/><br/>
<span>Effective Date:</span><br/><br/>
</td>
</tr>
<tr>
<td>
<span>Fast Track Projects Pvt. Ltd <br/> By:</span>
<span style="float: right;">Employee / Consultant/Recipient <br/> By:</span>
</td>
</tr>
<tr>
<td>
<br/><br/>
<span style="font-weight: bold"><t t-esc="o.manager_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.candidate_id.partner_name"></span><br/>
<span style="font-weight: bold"><t t-esc="o.manager_id.job_id.name"/></span><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.position"></span><br/>
<span style="font-weight: bold" t-esc="o.joining_date"/><span style="font-weight: bold;white-space: nowrap;float: right;" t-esc="o.joining_date"/>
<br/>
</td>
</tr>
</table>
</div>
</t>
</t>
</template>
</odoo>

View File

@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_offer_letter_user,offer.letter.user,model_offer_letter,base.group_user,1,1,1,0
access_offer_letter_manager,offer.letter.manager,model_offer_letter,hr.group_hr_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_offer_letter_user offer.letter.user model_offer_letter base.group_user 1 1 1 0
3 access_offer_letter_manager offer.letter.manager model_offer_letter hr.group_hr_manager 1 1 1 1

View File

@ -1,70 +0,0 @@
/** @odoo-module **/
import { Component, useState, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { CharField, charField } from "@web/views/fields/char/char_field";
// Local utility function to parse the field value
function parseData(value) {
try {
return JSON.parse(value);
} catch (e) {
console.warn("Failed to parse value in PayDetailsWidget:", value);
return [];
}
}
export class PayDetailsWidget extends CharField {
static props = {
...CharField.props,
resModel: { type: String, optional: true },
onlySearchable: { type: Boolean, optional: true },
followRelations: { type: Boolean, optional: true },
};
setup() {
this.state = useState({
lines: parseData(this.props.record.data.terms_conditions) || []
});
}
formatAmount(amount) {
return parseFloat(amount).toFixed(2);
}
}
PayDetailsWidget.template = xml`
<div class="o_pay_details_widget" align="center">
<table class="table table-sm table-striped table-hover">
<thead>
<tr>
<th>Sequence</th>
<th>Code</th>
<th>Name</th>
<th class="text-end">Amount</th>
<th class="text-end">Year Total</th>
</tr>
</thead>
<tbody>
<t t-set="n" t-value="0"/>
<t t-foreach="state.lines" t-as="line" t-key="line['code']">
<t t-set="n" t-value="n+1"/>
<tr t-att-class="{'text-danger': line['amount'] &lt; 0}">
<td><t t-esc="n"/></td>
<td><t t-esc="line['code']"/></td>
<td><t t-esc="line['name']"/></td>
<td class="text-end"><t t-esc="formatAmount(line['amount'])"/></td>
<td class="text-end"><t t-esc="formatAmount(line['y_amount'])"/></td>
</tr>
</t>
</tbody>
</table>
</div>
`;
export const PayDetailsWidgets = {
...charField,
component: PayDetailsWidget,
};
registry.category("fields").add("pay_details_widget", PayDetailsWidgets);

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="menu_offer_letters_list" name="Offer Letters" parent="hr_ftp.menu_hr_recruitment_hr" action="action_offer_letters"/>
</odoo>

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_offer_letter_list" model="ir.ui.view">
<field name="name">offer.letter.list</field>
<field name="model">offer.letter</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="candidate_id"/>
<field name="position"/>
<field name="state"/>
<field name="sent_date"/>
</list>
</field>
</record>
<record id="view_offer_letter_form" model="ir.ui.view">
<field name="name">offer.letter.form</field>
<field name="model">offer.letter</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_send_offer" type="object" string="Send Offer" class="oe_highlight" invisble="state != 'draft'"/>
<button name="action_accept_offer" type="object" string="Accept Offer" invisble="state != 'sent'" class="oe_highlight"/>
<button name="action_reject_offer" type="object" string="Reject Offer" invisble="state != 'sent'" class="oe_danger"/>
<button name="get_paydetailed_lines" type="object" string="Get Data" invisble="state != 'sent'" class="oe_danger"/>
<button name="generate_pdf_report" type="object" string="Generate PDF" class="oe_highlight"/>
<field name="state" widget="statusbar" statusbar_visible="draft,sent,accepted,rejected,expired"/>
</header>
<sheet>
<group>
<group>
<field name="name" readonly="state != 'draft'"/>
<field name="candidate_id"/>
<field name="manager_id"/>
<field name="position"/>
<field name="salary"/>
<field name="mi"/>
</group>
<group>
<field name="currency_id"/>
<field name="joining_date"/>
<field name="contract_type"/>
<field name="probation_period"/>
<field name="pay_struct_id"/>
</group>
</group>
<notebook>
<page string="Terms Conditions">
<field name="terms_conditions" widget="pay_details_widget"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_offer_letters" model="ir.actions.act_window">
<field name="name">Offer Letters</field>
<field name="res_model">offer.letter</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': "Multi Tabs",
'summary': "Multi Tabs for Odoo 18",
'description': """
Multi Tabs
""",
"author": "1311793927@qq.com",
'support': '1311793927qq.com',
'images': ['static/description/main_banner.png'],
'category': 'General',
'version': '0.1',
"license": "LGPL-3",
'depends': ['base','web'],
"installable": True,
"auto_install": False,
"assets": {
"web.assets_backend": [
"tabbar/static/src/**/*",
],
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,10 +0,0 @@
<section class="oe_container oe_slogan">
<span>
The basic functions have been implemented, and further optimization will be carried out later</span>
<br />
<br />
<img src="1.png" style="width:950px">
</section>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,189 +0,0 @@
import { ActionContainer } from '@web/webclient/actions/action_container';
import { patch } from '@web/core/utils/patch';
import { AklMultiTab } from './components/multi_tab/akl_multi_tab';
import { xml, useState } from '@odoo/owl';
import { browser } from '@web/core/browser/browser';
import { useService } from '@web/core/utils/hooks';
import {
router as _router,
} from '@web/core/browser/router';
patch(ActionContainer.prototype, {
setup() {
super.setup();
this.action_infos = [];
this.controllerStacks = {};
this.actionService = useService('action');
this.env.bus.addEventListener(
'ACTION_MANAGER:UPDATE',
({ detail: info }) => {
debugger
this.action_infos = this.get_controllers(info);
this.controllerStacks = info.controllerStacks;
this.render();
}
);
},
_on_close_action(action_info) {
this.action_infos = this.action_infos.filter((info) => {
return info.key !== action_info.key;
});
if (this.action_infos.length > 0) {
delete this.controllerStacks[action_info.key];
this.action_infos[this.action_infos.length - 1].active = true; // Set last
this.render();
}
},
get_controllers(info) {
const action_infos = [];
const entries = Object.entries(info.controllerStacks);
entries.forEach(([key, stack]) => {
const lastController = stack[stack.length - 1];
const action_info = {
key: key,
__info__: lastController,
// Store the exact router state that was active for this tab
routerState: lastController.state,
Component: lastController.__info__.Component,
active: false,
componentProps: lastController.__info__.componentProps || {},
}
if (lastController.count == info.count) {
action_info.active = true;
}
action_infos.push(action_info);
})
return action_infos;
},
_on_active_action(action_info) {
debugger;
this.action_infos.forEach((info) => {
info.active = info.key === action_info.key;
});
try {
// Use the exact router state that was saved
if (action_info.routerState) {
_router.pushState(action_info.routerState, { replace: true });
// Force the action service to reload the state
setTimeout(() => {
this.actionService.loadState();
}, 10);
}
} catch (e) {
console.error("Error switching controller stack:", e);
}
this.render();
},
// _on_active_action(action_info) {
// debugger
// this.action_infos.forEach((info) => {
// info.active = info.key === action_info.key;
// });
//
// // Get the action details from the action info
// const action = action_info.__info__.jsId ?
// { jsId: action_info.__info__.jsId } :
// action_info.__info__.action;
//
// // Get the controller from the controller stack
// const controllerStack = this.controllerStacks[action_info.key];
// if (!controllerStack || controllerStack.length === 0) {
// console.error("Cannot switch tabs: No controller found for tab", action_info);
// return;
// }
//
// const controller = controllerStack[controllerStack.length - 1];
//
//
// // Execute the action to switch context
// this.actionService.loadAction(action_info.__info__.action.id).then(loadedAction => {
// // Update the controller with the loaded action
// controller.action = loadedAction;
//
// // Execute the action to switch context
// this.actionService.doAction(loadedAction, {
// clear_breadcrumbs: false,
// pushState: false,
// replaceState: true, // Replace the current state instead of pushing a new one
// });
//
// try {
// const url = _router.stateToUrl(action_info.__info__.state);
// browser.history.pushState({}, "", url);
// } catch (e) {
// console.error("Error updating URL:", e);
// }
// this.render();
// });
// return;
//
// const url = _router.stateToUrl(action_info.__info__.state)
// browser.history.pushState({}, "", url);
// this.render();
// },
_close_other_action() {
this.action_infos = this.action_infos.filter((info) => {
if (info.active == false) {
delete this.controllerStacks[info.key];
}
return info.active == true
});
this.render();
},
_close_current_action() {
this.action_infos = this.action_infos.filter((info) => {
if (info.active == true) {
delete this.controllerStacks[info.key];
}
return info.active == false
});
this.action_infos[this.action_infos.length - 1].active = true;
this.render();
},
_on_close_all_action() {
this.action_infos.forEach((info) => {
delete this.controllerStacks[info.key];
});
this.action_infos = {}
window.location.href = "/";
}
});
ActionContainer.components = {
...ActionContainer.components,
AklMultiTab,
};
ActionContainer.template = xml`
<t t-name="web.ActionContainer">
<t t-set="action_infos" t-value="action_infos" />
<div class="o_action_manager d-flex flex-colum">
<AklMultiTab
action_infos="action_infos"
active_action="(action_info) => this._on_active_action(action_info)"
close_action="(action_info) => this._on_close_action(action_info)"
close_current_action="() => this._close_current_action()"
close_other_action="() => this._close_other_action()"
close_all_action="() => this._on_close_all_action()"
/>
<div t-foreach="action_infos" t-as="action_info" t-if="action_info" t-key="action_info.key" class="akl_controller_container d-flex flex-column" t-att-class="action_info.active ? '' : 'd-none'" >
<t t-component="action_info.Component" className="'o_action'" t-props="action_info.componentProps" />
</div>
</div>
</t>
`;

View File

@ -1,29 +0,0 @@
.akl_controller_container {
overflow-y: hidden;
flex: 1 1 auto;
.o_view_controller {
display: flex;
height: 100%;
overflow: hidden;
flex-direction: column;
.o_content {
flex: 1 1 auto;
overflow-y: auto;
}
}
.o_action {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: white;
.o_content {
flex: 1 1 auto;
overflow-y: auto;
background-color: white;
}
}
}

Some files were not shown because too many files have changed in this diff Show More