Compare commits
52 Commits
feature/od
...
ats_web
| Author | SHA1 | Date |
|---|---|---|
|
|
b479fee7c9 | |
|
|
672551cd0a | |
|
|
0b93dd1a87 | |
|
|
e88f7da488 | |
|
|
79d14a2023 | |
|
|
0193fe04fb | |
|
|
b10e096540 | |
|
|
a8df849cc3 | |
|
|
d154a96b2b | |
|
|
8c6b7669a3 | |
|
|
d56eaaa306 | |
|
|
8c4e5d82da | |
|
|
6d9933c2a1 | |
|
|
2084217fbc | |
|
|
2aef56be49 | |
|
|
4194fbfecf | |
|
|
95a50af0f3 | |
|
|
a64a2f8016 | |
|
|
4f4b7e0c36 | |
|
|
6b97411909 | |
|
|
2896e9e83b | |
|
|
785a7da404 | |
|
|
c21f46bf33 | |
|
|
d4f31b5af5 | |
|
|
1e9485bb5a | |
|
|
541272a7c1 | |
|
|
5b0d3f4a3e | |
|
|
b195382233 | |
|
|
709e17ad7b | |
|
|
b9c7d43541 | |
|
|
3207b47f72 | |
|
|
5fdb26226c | |
|
|
640ddcdaa5 | |
|
|
6053b90162 | |
|
|
54fafbb1e9 | |
|
|
f9985e9c58 | |
|
|
c5c89709e4 | |
|
|
871ad34f7a | |
|
|
7188c475d5 | |
|
|
e650a1f1bf | |
|
|
d9f2183c2b | |
|
|
19f54e5ba0 | |
|
|
3e6eb7799b | |
|
|
4a2026faae | |
|
|
5cdbdd75b0 | |
|
|
f7c05c7b2e | |
|
|
6273e2fa96 | |
|
|
7600729b8b | |
|
|
25aa25c6f5 | |
|
|
a0961f338b | |
|
|
196f7fb371 | |
|
|
e7a8762880 |
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import maintenance_equipment
|
||||
from . import res_company
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import custom_qrcode_generator
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 9.2 KiB |
|
|
@ -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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import equipment_label_layout
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
from . import models, controllers
|
||||
from . import models
|
||||
|
|
@ -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)}"
|
||||
|
|
@ -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>
|
||||
|
|
@ -66,8 +66,8 @@ class AttendanceReport(models.Model):
|
|||
%s::date,
|
||||
interval '1 day'
|
||||
)::date AS date
|
||||
),
|
||||
employee_dates AS (
|
||||
),
|
||||
employee_dates AS (
|
||||
SELECT
|
||||
emp.id AS employee_id,
|
||||
emp.name AS employee_name,
|
||||
|
|
@ -86,8 +86,8 @@ employee_dates AS (
|
|||
WHERE
|
||||
emp.active = true
|
||||
""" + (" AND " + " AND ".join(emp_date_conditions) if emp_date_conditions else "") + """
|
||||
),
|
||||
daily_checkins AS (
|
||||
),
|
||||
daily_checkins AS (
|
||||
SELECT
|
||||
emp.id,
|
||||
emp.name,
|
||||
|
|
@ -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
|
||||
|
|
@ -107,8 +106,8 @@ daily_checkins AS (
|
|||
hr_department dep ON emp.department_id = dep.id
|
||||
WHERE
|
||||
""" + " AND ".join(checkin_conditions) + """
|
||||
),
|
||||
attendance_summary AS (
|
||||
),
|
||||
attendance_summary AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
|
|
@ -116,17 +115,13 @@ 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
|
||||
GROUP BY
|
||||
id, name, date, department
|
||||
),
|
||||
leave_data AS (
|
||||
),
|
||||
leave_data AS (
|
||||
SELECT
|
||||
hl.employee_id,
|
||||
hl.date_from AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS leave_start,
|
||||
|
|
@ -142,8 +137,8 @@ leave_data AS (
|
|||
WHERE
|
||||
hl.state IN ('validate', 'confirm', 'validate1')
|
||||
AND (hl.date_from, hl.date_to) OVERLAPS (%s::timestamp, %s::timestamp)
|
||||
)
|
||||
SELECT
|
||||
)
|
||||
SELECT
|
||||
ed.employee_id AS id,
|
||||
ed.employee_name AS name,
|
||||
ed.date,
|
||||
|
|
@ -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
|
||||
|
|
@ -160,17 +154,18 @@ SELECT
|
|||
WHEN ats.first_check_in IS NOT NULL THEN 'Present'
|
||||
ELSE 'NA'
|
||||
END AS status
|
||||
FROM
|
||||
FROM
|
||||
employee_dates ed
|
||||
LEFT JOIN
|
||||
LEFT JOIN
|
||||
attendance_summary ats ON ed.employee_id = ats.id AND ed.date = ats.date
|
||||
LEFT JOIN
|
||||
LEFT JOIN
|
||||
leave_data ld ON ed.employee_id = ld.employee_id
|
||||
AND ed.date >= DATE(ld.leave_start)
|
||||
AND ed.date <= DATE(ld.leave_end)
|
||||
ORDER BY
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 > 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 > 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 > 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 >= 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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
,,,,,,,
|
||||
|
|
|
@ -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 && presentStreet) permanentStreet.value = presentStreet.value;
|
||||
if (permanentStreet2 && presentStreet2) permanentStreet2.value = presentStreet2.value;
|
||||
if (permanentCity && presentCity) permanentCity.value = presentCity.value;
|
||||
if (permanentZip && 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 && 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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# __manifest__.py
|
||||
{
|
||||
'name': 'Recruitment web app',
|
||||
'version': '18.0',
|
||||
'category': 'Tools',
|
||||
"author": "FTPROTECH PVT LTD",
|
||||
"website": "https://www.ftprotech.in/",
|
||||
'summary': 'Extracts the information of candidates from the resumes and creates applications in recruitment.',
|
||||
'depends': ['base', 'web', 'web_editor', 'sms', 'hr_recruitment', 'hr_recruitment_extended', 'base_setup','website_hr_recruitment_extended'],
|
||||
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'hr_recruitment_web_app/static/lib/ckeditor/ckeditor.js',
|
||||
|
||||
'hr_recruitment_web_app/static/src/js/ats.js',
|
||||
'hr_recruitment_web_app/static/src/js/job_requests.js',
|
||||
'hr_recruitment_web_app/static/src/js/applicants.js',
|
||||
'hr_recruitment_web_app/static/src/css/candidate.css',
|
||||
#
|
||||
#
|
||||
'hr_recruitment_web_app/static/src/css/colors.css',
|
||||
'hr_recruitment_web_app/static/src/css/ats.css',
|
||||
'hr_recruitment_web_app/static/src/css/list.css',
|
||||
'hr_recruitment_web_app/static/src/css/content.css',
|
||||
'hr_recruitment_web_app/static/src/css/applicants.css',
|
||||
'hr_recruitment_web_app/static/src/css/jd.css',
|
||||
# 'hr_recruitment_web_application/static/src/css/applicants_details.css',
|
||||
# 'hr_recruitment_web_application/static/src/css/ats_candidate.css',
|
||||
],
|
||||
},
|
||||
|
||||
'data': [
|
||||
"security/ir.model.access.csv",
|
||||
'views/recruitmnet_doc_upload_wizard.xml',
|
||||
'views/hr_candidate.xml',
|
||||
'views/res_config_view.xml',
|
||||
'views/main.xml',
|
||||
'views/recruitment.xml',
|
||||
'views/jd.xml',
|
||||
'views/applicants.xml',
|
||||
'views/candidate.xml',
|
||||
|
||||
],
|
||||
'images': ['static/description/banner.png'],
|
||||
'external_dependencies': {
|
||||
'python': ['pytesseract', 'pdf2image', 'pypdf'],
|
||||
},
|
||||
'installable': True,
|
||||
'application': True,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import web_recruitment
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import recruitment_doc_upload_wizard
|
||||
from . import res_config_settings
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
from odoo import models, fields, api
|
||||
from pypdf import PdfReader
|
||||
from datetime import datetime
|
||||
import base64
|
||||
import re
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import logging
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from pdf2image import convert_from_bytes
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
import json
|
||||
# from docx import Document
|
||||
# import binascii
|
||||
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecruitmentDocUploadWizard(models.TransientModel):
|
||||
_name = 'recruitment.doc.upload.wizard'
|
||||
_description = 'Recruitment Document Upload Wizard'
|
||||
|
||||
# Define the fields in the wizard
|
||||
name = fields.Char("Name")
|
||||
json_data = fields.Text("Json Data")
|
||||
file_name = fields.Char('File Name')
|
||||
file_data = fields.Binary('File Data', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
mimetype = fields.Char(string="Type", readonly=True)
|
||||
file_html_text = fields.Html()
|
||||
|
||||
def compute_mimetype(self):
|
||||
for record in self:
|
||||
record.mimetype = ''
|
||||
if record.file_data:
|
||||
try:
|
||||
# Fix padding
|
||||
padded_data = record.file_data + b'=' * (-len(record.file_data) % 4)
|
||||
binary = base64.b64decode(padded_data)
|
||||
record.mimetype = guess_mimetype(binary)
|
||||
except Exception:
|
||||
record.mimetype = 'Invalid base64'
|
||||
|
||||
def action_upload(self):
|
||||
# Implement the logic for file upload here
|
||||
# You can use the fields file_data, file_name, etc., to save the data in the desired model
|
||||
pass
|
||||
|
||||
|
||||
def action_fetch_json(self):
|
||||
for record in self:
|
||||
record.compute_mimetype()
|
||||
if not record.file_data:
|
||||
record.json_data = "No file content provided."
|
||||
continue
|
||||
|
||||
binary = base64.b64decode(record.file_data)
|
||||
file_type = record.mimetype
|
||||
text_content = ""
|
||||
|
||||
try:
|
||||
if file_type == "application/pdf":
|
||||
try:
|
||||
pdf_reader = PdfReader(BytesIO(binary))
|
||||
for page in pdf_reader.pages:
|
||||
page_text = page.extract_text()
|
||||
text_content += page_text
|
||||
if not text_content:
|
||||
images = convert_from_bytes(binary, dpi=300)
|
||||
extracted_text = []
|
||||
for image in images:
|
||||
text = pytesseract.image_to_string(image)
|
||||
extracted_text.append(text)
|
||||
text_content = "\n".join(extracted_text)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Error reading PDF: %s", str(e))
|
||||
text_content = ""
|
||||
|
||||
elif file_type in ["image/png", "image/jpeg", "image/jpg"]:
|
||||
try:
|
||||
image = Image.open(BytesIO(binary))
|
||||
text_content = pytesseract.image_to_string(image)
|
||||
except Exception as e:
|
||||
_logger.error("Error processing image: %s", str(e))
|
||||
text_content = ""
|
||||
|
||||
elif file_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
||||
try:
|
||||
doc = Document(BytesIO(binary))
|
||||
text_content = ''
|
||||
|
||||
for section in doc.sections:
|
||||
header = section.header
|
||||
for paragraph in header.paragraphs:
|
||||
text_content += paragraph.text + '\n'
|
||||
|
||||
for paragraph in doc.paragraphs:
|
||||
text_content += paragraph.text + '\n'
|
||||
except Exception as e:
|
||||
_logger.error("Error processing DOCX: %s", str(e))
|
||||
text_content = ""
|
||||
|
||||
else:
|
||||
_logger.error("Unsupported file type: %s", file_type)
|
||||
record.json_data = f"Unsupported file type: {file_type}"
|
||||
continue
|
||||
record.file_html_text = text_content
|
||||
json_response = record.get_json_from_model(text_content)
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
if json_response and "choices" in json_response and len(json_response["choices"]) > 0:
|
||||
message_content = json_response["choices"][0].get("message", {}).get("content", "")
|
||||
|
||||
if message_content:
|
||||
match = re.search(r'```json\n(.*?)\n```', message_content, re.DOTALL)
|
||||
if match:
|
||||
clean_json_str = match.group(1).strip() # Extract JSON content
|
||||
try:
|
||||
parsed_json = json.loads(clean_json_str)
|
||||
record.json_data = json.dumps(parsed_json, indent=4)
|
||||
file_name = parsed_json.get("name", "")
|
||||
if file_name:
|
||||
self.write({'file_name': file_name})
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
_logger.error("Error parsing JSON: %s", str(e))
|
||||
record.json_data = "Error parsing JSON"
|
||||
else:
|
||||
_logger.error("No valid JSON found in the content")
|
||||
record.json_data = "No valid JSON format found"
|
||||
else:
|
||||
_logger.error("No message content found in the response")
|
||||
record.json_data = "No message content found"
|
||||
else:
|
||||
_logger.error("No valid response or choices in the API response")
|
||||
record.json_data = "No valid JSON data received."
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Unexpected error during OCR processing: %s", str(e))
|
||||
record.json_data = "An unexpected error occurred during file processing."
|
||||
|
||||
_logger.info("Stored JSON data for file: %s", record.file_name)
|
||||
|
||||
|
||||
def normalize_gender(self, gender):
|
||||
if gender:
|
||||
return gender.replace(" ", "").lower()
|
||||
return gender
|
||||
|
||||
def normalize_marital_status(self, marital_status):
|
||||
if marital_status:
|
||||
return marital_status.replace(" ", "").lower()
|
||||
return marital_status
|
||||
|
||||
def parse_experience(self, experience_str):
|
||||
years = 0
|
||||
months = 0
|
||||
|
||||
year_match = re.search(r'(\d+)\s*[\+]*\s*years?', experience_str, re.IGNORECASE)
|
||||
month_match = re.search(r'(\d+)\s*months?', experience_str, re.IGNORECASE)
|
||||
|
||||
if year_match:
|
||||
years = int(year_match.group(1))
|
||||
if month_match:
|
||||
months = int(month_match.group(1))
|
||||
|
||||
return years, months
|
||||
|
||||
def get_json_from_model(self, text_content):
|
||||
print(text_content)
|
||||
api_url = "https://api.together.xyz/v1/chat/completions"
|
||||
together_api_key = self.env['ir.config_parameter'].sudo().get_param('hr_recruitment_web_app.together_api_key')
|
||||
headers = {
|
||||
'Authorization': 'Bearer %s' % together_api_key,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
current_date = datetime.now()
|
||||
previous_month_date = current_date - relativedelta(months=1)
|
||||
previous_month_year = previous_month_date.strftime("%B %Y")
|
||||
payload = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "provide the json data from the above content for below fields---\n\nname-- particularly the full name of the candidate\nskills-- the skills of the candidate mentioned in the text specifically under skills section (add both soft and technical skills in this). \nemail -- the contact email of the candidate \nphone -- contact number of candidate usually a 10-12 number digits\ndegree-- the degree or qualification of the candidate mentioned in resume (only the name of the degrees or qualifications and not the whole details) "
|
||||
f"\n experience in years and months in the format: Title of the experience (type of experience) (from month/year to month/year) -> years and months (If the end date is marked as 'present' or 'till now', assume today's date is {previous_month_year} and calculate the months also properly). \n"
|
||||
"\n Total Experience (Non-Overlapping) : in years and months \n"
|
||||
"\n location-- search for the location or place mentioned in the resume where the candidate belong to. \n gender-- the gender of the candidate if mentioned. \ndate_of_birth-- the date of birth of the candidate in the format-%d/%m/%Y \nmarital_status-- the marital status of the candidate \n languages-- the languages known by the candidate mentioned in resume(not technical languages but the spoken ones specifically mentioned under languages section)\n"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": text_content
|
||||
}
|
||||
],
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
json_response = response.json()
|
||||
return json_response
|
||||
except ValueError as e:
|
||||
_logger.error("Error parsing JSON: %s", str(e))
|
||||
return {}
|
||||
else:
|
||||
_logger.error("Error in API call: %s", response.text)
|
||||
return {}
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Exception during API call: %s", str(e))
|
||||
return {}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
together_api_key = fields.Char(config_parameter='hr_recruitment_web_app.together_api_key', string="Together API key")
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_recruitment_doc_upload_wizard_user,recruitment.doc.upload.wizard user,model_recruitment_doc_upload_wizard,base.group_user,1,1,1,1
|
||||
|
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,667 @@
|
|||
@import url('colors.css');
|
||||
|
||||
|
||||
/* ====== candidate creation template ======= */
|
||||
|
||||
/* ===== Candidate Form Styles ===== */
|
||||
.candidate-form-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.candidate-form-modal.show {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-content {
|
||||
background-color: var(--white);
|
||||
width: 90%;
|
||||
max-width: 1400px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 5px 20px var(--shadow-dark);
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.candidate-form-modal.show .candidate-form-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-header {
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
|
||||
color: var(--white);
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.candidate-form-modal .header-icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .header-icon {
|
||||
font-size: 24px;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-close {
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
color: var(--white);
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-close:hover {
|
||||
transform: scale(1.2);
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-body {
|
||||
padding: 25px;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Header Section with Avatar */
|
||||
.candidate-form-modal .header-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-avatar {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--primary-blue);
|
||||
box-shadow: 0 3px 10px var(--shadow-color);
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-upload {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-upload:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-upload i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-upload input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .basic-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Button Box Section */
|
||||
.candidate-form-modal .button-box {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-button {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 15px;
|
||||
background-color: var(--gray-50);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-button:hover {
|
||||
background-color: var(--gray-100);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 2px 5px var(--shadow-color);
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-button i {
|
||||
font-size: 24px;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Form Sections */
|
||||
.candidate-form-modal .form-section {
|
||||
background-color: var(--white);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 5px var(--shadow-color);
|
||||
border-left: 4px solid var(--secondary-purple);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .section-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .section-title i {
|
||||
color: var(--secondary-purple);
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-group label {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-input,
|
||||
.candidate-form-modal .form-select,
|
||||
.candidate-form-modal .form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
background-color: var(--white);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-input:focus,
|
||||
.candidate-form-modal .form-select:focus,
|
||||
.candidate-form-modal .form-textarea:focus {
|
||||
border-color: var(--secondary-purple);
|
||||
box-shadow: 0 0 0 3px var(--primary-blue-light);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Resume Section */
|
||||
.candidate-form-modal .resume-upload-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-area {
|
||||
flex: 1;
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-area:hover {
|
||||
border-color: var(--primary-blue);
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.candidate-form-modal .upload-icon {
|
||||
font-size: 40px;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-area h5 {
|
||||
margin: 0 0 5px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-area p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-preview {
|
||||
flex: 1;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-preview-placeholder i {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-danger {
|
||||
background-color: var(--danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-danger:hover {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
/* Make sure dropzone doesn't interfere with child elements */
|
||||
.candidate-form-modal .resume-upload-area > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-area input {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.candidate-form-modal #resume-iframe {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal #resume-image {
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.candidate-form-modal #unsupported-format {
|
||||
text-align: center;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.candidate-form-modal #unsupported-format i {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.candidate-form-modal #download-resume {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Skills Section */
|
||||
.candidate-form-modal .skills-container {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .skills-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .skill-tag {
|
||||
background-color: var(--primary-blue);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .skill-tag i {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-add-skill {
|
||||
background-color: transparent;
|
||||
color: var(--primary-blue);
|
||||
border: 1px solid var(--primary-blue);
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-add-skill:hover {
|
||||
background-color: var(--primary-blue-light);
|
||||
}
|
||||
|
||||
/* Notebook Tabs */
|
||||
.candidate-form-modal .notebook-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.candidate-form-modal .tab.active {
|
||||
border-bottom-color: var(--primary-blue);
|
||||
color: var(--primary-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.candidate-form-modal .tab:hover:not(.active) {
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.candidate-form-modal .notebook-content {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Sub-sections in notebook */
|
||||
.candidate-form-modal .sub-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .sub-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .sub-section-title i {
|
||||
color: var(--secondary-purple);
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
.candidate-form-modal .form-actions {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: var(--white);
|
||||
padding: 15px 25px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
z-index: 100;
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-cancel {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
background-color: var(--gray-100);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-cancel:hover {
|
||||
background-color: var(--gray-200);
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-candidate-primary {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-candidate-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px var(--shadow-color);
|
||||
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
|
||||
}
|
||||
|
||||
.candidate-form-modal .footer-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container {
|
||||
z-index: 10000 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container--default .select2-selection--multiple,
|
||||
.candidate-form-modal .select2-container--default .select2-selection--single {
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 4px !important;
|
||||
min-height: 40px;
|
||||
background-color: var(--white);
|
||||
padding: 5px 5px 0 5px !important;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: var(--primary-blue) !important;
|
||||
border: none !important;
|
||||
color: var(--white) !important;
|
||||
padding: 2px 8px !important;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.application-creation-modal .select2-dropdown {
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: 0 2px 5px var(--shadow-color) !important;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container--default .select2-selection__rendered {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
vertical-align: middle !important;
|
||||
line-height: normal !important;
|
||||
color: var(--text-primary);
|
||||
padding: 2px 5px !important;
|
||||
}
|
||||
.candidate-form-modal .select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
align-items: center !important;
|
||||
width: 100% !important;
|
||||
padding: 2px 5px !important;
|
||||
overflow: visible !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.candidate-form-modal .select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: var(--primary-blue) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.candidate-form-modal .candidate-form-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-track {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-blue);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.candidate-form-modal .candidate-form-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-blue-dark);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.candidate-form-modal .btn-candidate-primary {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue) 100%);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.candidate-form-modal .btn-candidate-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px var(--shadow-color);
|
||||
background: linear-gradient(135deg, var(--secondary-purple) 0%, var(--primary-blue-dark) 100%);
|
||||
}
|
||||
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.candidate-form-modal .candidate-form-content {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.candidate-form-modal .resume-upload-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.candidate-form-modal .candidate-form-content {
|
||||
width: 98%;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.candidate-form-modal .form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.candidate-form-modal .avatar-container {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.candidate-form-modal .button-box {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.candidate-form-modal .stat-button {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-blue: #3498db;
|
||||
--primary-blue-dark: #2980b9;
|
||||
--primary-blue-light: #e8f0ff;
|
||||
|
||||
/* Secondary Colors */
|
||||
--secondary-purple: #6f42c1;
|
||||
--secondary-green: #28a745;
|
||||
--secondary-red: #dc3545;
|
||||
--secondary-yellow: #ffc107;
|
||||
|
||||
/* Grayscale */
|
||||
--white: #ffffff;
|
||||
--gray-100: #f8f9fa;
|
||||
--gray-200: #e9ecef;
|
||||
--gray-300: #dee2e6;
|
||||
--gray-400: #ced4da;
|
||||
--gray-500: #adb5bd;
|
||||
--gray-600: #6c757d;
|
||||
--gray-700: #495057;
|
||||
--gray-800: #343a40;
|
||||
--gray-900: #212529;
|
||||
--black: #000000;
|
||||
|
||||
/* Semantic Colors */
|
||||
--success: #28a745;
|
||||
--info: #17a2b8;
|
||||
--warning: #ffc107;
|
||||
--danger: #dc3545;
|
||||
|
||||
/* Background Colors */
|
||||
--body-bg: #f5f6fa;
|
||||
--sidebar-bg: #FAFCFF;
|
||||
--create-model-bg: #FAFCFF;
|
||||
--content-bg: #E3E9EF;
|
||||
--active-search-bg: #ffffff;
|
||||
--active-search-hover-bg: #f0f0f0;
|
||||
--add-btn-bg: #3498db;
|
||||
--add-btn-hover-bg: #2980b9;
|
||||
--side-panel-bg: #FAFCFF;
|
||||
--side-panel-item-hover: #f0f8ff;
|
||||
--side-panel-item-selected: #d7eaff;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #2f3542;
|
||||
--text-secondary: #4B5865;
|
||||
--text-muted: #6c757d;
|
||||
--sidebar-text: #0F1419;
|
||||
--active-search-text: #333;
|
||||
--add-btn-color: #ffffff;
|
||||
|
||||
/* Border Colors */
|
||||
--border-color: #dcdde1;
|
||||
--border-light: #e0e0e0;
|
||||
|
||||
/* Shadow Colors */
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--shadow-dark: rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* Status Colors */
|
||||
--status-new: #3498db;
|
||||
--status-interview: #f39c12;
|
||||
--status-hired: #2ecc71;
|
||||
--status-rejected: #e74c3c;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 118 KiB |
|
|
@ -0,0 +1,850 @@
|
|||
/** @odoo-module **/
|
||||
function initApplicantsPage() {
|
||||
console.log("Applicants Page Loaded");
|
||||
const applicantDetailArea = document.getElementById("applicants-detail");
|
||||
const container = document.querySelector('.ats-list-container');
|
||||
const toggleBtn = document.getElementById("applicants-list-sidebar-toggle-btn");
|
||||
const sidebar = document.getElementById("applicants-list-panel");
|
||||
|
||||
// Fix: Use correct class for applicant items - using both classes
|
||||
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
|
||||
item.addEventListener("click", function() {
|
||||
console.log("Applicant item clicked"); // Add this for debugging
|
||||
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
|
||||
this.classList.add("selected");
|
||||
|
||||
applicantDetailArea.style.display = 'block';
|
||||
container.classList.add('ats-selected');
|
||||
sidebar.classList.remove('collapsed');
|
||||
toggleBtn.style.display = 'flex';
|
||||
|
||||
const applicantId = this.dataset.id;
|
||||
console.log("Applicant ID:", applicantId); // Add this for debugging
|
||||
|
||||
// Show loading state
|
||||
if (applicantDetailArea) {
|
||||
applicantDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading applicant details...</p></div>';
|
||||
}
|
||||
|
||||
fetch(`/myATS/applicant/detail/${applicantId}`, {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Network response was not ok');
|
||||
return res.text();
|
||||
})
|
||||
.then(html => {
|
||||
console.log("Response received"); // Add this for debugging
|
||||
if (applicantDetailArea) {
|
||||
applicantDetailArea.innerHTML = html;
|
||||
initApplicantDetailEdit(); // Initialize edit functionality
|
||||
|
||||
// Add close button functionality
|
||||
const closeBtn = applicantDetailArea.querySelector('.close-detail');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
applicantDetailArea.style.display = 'none';
|
||||
container.classList.remove('ats-selected');
|
||||
document.querySelectorAll(".ats-item.applicants-item.selected").forEach(el => el.classList.remove("selected"));
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading applicant details:', error);
|
||||
if (applicantDetailArea) {
|
||||
applicantDetailArea.innerHTML = '<div class="alert alert-danger">Error loading applicant details. Please try again.</div>';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Search functionality - use correct ID
|
||||
const search = document.getElementById("applicants-search");
|
||||
if (search) {
|
||||
search.addEventListener("input", function() {
|
||||
const query = this.value.toLowerCase();
|
||||
let visibleCount = 0;
|
||||
// Also fix this selector to use both classes
|
||||
document.querySelectorAll(".ats-item.applicants-item").forEach(item => {
|
||||
const match = item.textContent.toLowerCase().includes(query);
|
||||
item.style.display = match ? "" : "none";
|
||||
if (match) visibleCount++;
|
||||
});
|
||||
const countElement = document.getElementById("active-records-count");
|
||||
if (countElement) {
|
||||
countElement.textContent = visibleCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sidebar Toggle
|
||||
if (toggleBtn && sidebar) {
|
||||
toggleBtn.addEventListener("click", function(e) {
|
||||
e.stopPropagation();
|
||||
sidebar.classList.toggle("collapsed");
|
||||
});
|
||||
}
|
||||
|
||||
// Applicant Modal Handling
|
||||
const modal = document.querySelector('.applicant-detail-modal');
|
||||
if (modal) {
|
||||
const closeModal = modal.querySelector('.applicant-close-modal');
|
||||
|
||||
// Event delegation for image clicks - use correct classes
|
||||
document.addEventListener('click', function(e) {
|
||||
const img = e.target.closest('.ats-item-image, .ats-item-initials');
|
||||
if (!img) return;
|
||||
|
||||
const applicantItem = img.closest('.ats-item.applicants-item');
|
||||
if (!applicantItem) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Get applicant data with null checks
|
||||
const applicantId = applicantItem.dataset.id;
|
||||
const nameElement = applicantItem.querySelector('.ats-title');
|
||||
const jobElement = applicantItem.querySelector('#job_request');
|
||||
const stageElement = applicantItem.querySelector('#applicant_stage');
|
||||
const createDate = applicantItem.querySelector('#create_date');
|
||||
const applicantEmail = applicantItem.querySelector('#applicant_email');
|
||||
const applicantPhone = applicantItem.querySelector('#applicant_phone');
|
||||
const alternatePhone = applicantItem.querySelector('#alternate_phone');
|
||||
const recruiterName = applicantItem.querySelector('#recruiter_name');
|
||||
const recruiterImage = applicantItem.querySelector('#recruiter_image');
|
||||
const recruiterUserId = applicantItem.querySelector('#recruiter_id');
|
||||
|
||||
const name = nameElement ? nameElement.textContent : 'N/A';
|
||||
const job = jobElement ? jobElement.textContent : 'N/A';
|
||||
const stage = stageElement ? stageElement.textContent : 'N/A';
|
||||
const date = createDate ? createDate.textContent : 'N/A';
|
||||
const email = applicantEmail ? applicantEmail.textContent : 'N/A';
|
||||
const phone = applicantPhone ? applicantPhone.textContent : 'N/A';
|
||||
const altPhone = alternatePhone ? alternatePhone.textContent : 'N/A';
|
||||
const recruiter = recruiterName ? recruiterName.textContent : 'N/A';
|
||||
const recruiterId = recruiterUserId ? recruiterUserId.textContent: null;
|
||||
const photoSrc = img.classList.contains('ats-item-image') ? img.src : null;
|
||||
const initials = img.classList.contains('ats-item-initials') ? img.textContent : null;
|
||||
|
||||
// Populate modal with applicant data
|
||||
modal.querySelector('.modal-applicant-name').textContent = name;
|
||||
modal.querySelector('.modal-applicant-job').textContent = job;
|
||||
modal.querySelector('.modal-applicant-stage').textContent = stage;
|
||||
modal.querySelector('.modal-applicant-date').textContent = date;
|
||||
modal.querySelector('.modal-applicant-email').textContent = email;
|
||||
modal.querySelector('.modal-applicant-phone').textContent = phone;
|
||||
modal.querySelector('.modal-applicant-altphone').textContent = altPhone;
|
||||
|
||||
// Handle applicant image
|
||||
if (photoSrc) {
|
||||
const modalImg = modal.querySelector('.modal-applicant-image');
|
||||
modalImg.src = photoSrc;
|
||||
modalImg.style.display = 'block';
|
||||
modal.querySelector('.modal-applicant-initials').style.display = 'none';
|
||||
} else {
|
||||
modal.querySelector('.modal-applicant-initials').textContent = initials;
|
||||
modal.querySelector('.modal-applicant-initials').style.display = 'flex';
|
||||
modal.querySelector('.modal-applicant-image').style.display = 'none';
|
||||
}
|
||||
|
||||
// Handle recruiter info
|
||||
const recruiterImageEl = modal.querySelector('.recruiter-image');
|
||||
const recruiterInitialsEl = modal.querySelector('.recruiter-initials');
|
||||
const recruiterTooltip = modal.querySelector('.recruiter-tooltip');
|
||||
|
||||
if (recruiterImage && recruiterId) {
|
||||
const recruiterSrc = `/web/image/res.users/${recruiterId}/image_128`;
|
||||
recruiterImageEl.src = recruiterSrc;
|
||||
recruiterImageEl.style.display = 'block';
|
||||
recruiterInitialsEl.style.display = 'none';
|
||||
recruiterSrc.onerror = function () {
|
||||
recruiterImageEl.style.display = 'none';
|
||||
};
|
||||
} else if (recruiterName) {
|
||||
recruiterInitialsEl.textContent = recruiterName.textContent.charAt(0).toUpperCase();
|
||||
recruiterInitialsEl.style.display = 'flex';
|
||||
recruiterImageEl.style.display = 'none';
|
||||
}
|
||||
recruiterTooltip.textContent = recruiter;
|
||||
|
||||
// Set status ribbon class based on stage - use correct selector
|
||||
const statusRibbon = modal.querySelector('.applicant-status-ribbon');
|
||||
if (statusRibbon) {
|
||||
// Remove all existing status classes
|
||||
statusRibbon.classList.remove('new', 'interview', 'hired', 'rejected');
|
||||
// Add appropriate class based on stage
|
||||
if (stage.toLowerCase().includes('interview')) {
|
||||
statusRibbon.classList.add('interview');
|
||||
} else if (stage.toLowerCase().includes('hired')) {
|
||||
statusRibbon.classList.add('hired');
|
||||
} else if (stage.toLowerCase().includes('reject')) {
|
||||
statusRibbon.classList.add('rejected');
|
||||
} else {
|
||||
statusRibbon.classList.add('new');
|
||||
}
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
modal.classList.add('show');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
// Close modal handlers
|
||||
closeModal.addEventListener('click', function() {
|
||||
modal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
modal.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Close with ESC key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && modal.classList.contains('show')) {
|
||||
modal.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createApplication = document.getElementById('add-application-create-btn');
|
||||
const applicantModal = document.getElementById('application-creation-modal');
|
||||
const closeModal = document.querySelectorAll('.application-creation-close, .btn-cancel');
|
||||
if (createApplication) {
|
||||
createApplication.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
applicantModal.style.display = 'flex';
|
||||
setTimeout(() => {
|
||||
applicantModal.classList.add('show');
|
||||
}, 10);
|
||||
document.body.style.overflow = 'hidden';
|
||||
setTimeout(() => {
|
||||
initSelect2();
|
||||
initResumeUploadHandlers();
|
||||
}, 100);
|
||||
setTimeout(createApplicationForm, 100);
|
||||
});
|
||||
}
|
||||
if (closeModal) {
|
||||
closeModal.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
applicantModal.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
applicantModal.style.display = 'none';
|
||||
}, 300);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
// Close modal when clicking outside of it
|
||||
// applicantModal.addEventListener('click', function(e) {
|
||||
// if (e.target === applicantModal) {
|
||||
// applicantModal.classList.remove('show');
|
||||
// setTimeout(() => {
|
||||
// applicantModal.style.display = 'none';
|
||||
// }, 300);
|
||||
// document.body.style.overflow = '';
|
||||
// }
|
||||
// });
|
||||
// File Upload Handling
|
||||
const resumeUpload = document.getElementById('resume-upload');
|
||||
const resumeDropzone = document.getElementById('resume-dropzone');
|
||||
const resumePreview = document.getElementById('resume-preview');
|
||||
const resumePlaceholder = 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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
7
addons_extensions/hr_recruitment_web_app/static/src/libs/ckeditor/build/ckeditor.js
vendored
Normal 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">&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&id=' + str(candidate.id) + '&field=resume&download=false'"
|
||||
class="btn btn-sm btn-outline-primary me-2 document-action-btn attachment-btn preview-btn"
|
||||
target="_blank">
|
||||
<i class="fa fa-eye me-1"></i>Preview
|
||||
</a>
|
||||
<!-- Download Button -->
|
||||
<a t-if="candidate.resume"
|
||||
t-att-href="'/web/content/?model=hr.candidate&id=' + str(candidate.id) + '&field=resume&download=true'"
|
||||
class="btn btn-sm btn-outline-success document-action-btn attachment-btn download-btn">
|
||||
<i class="fa fa-download me-1"></i>Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ats-card span-3" id="candidate-availability">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-calendar me-2"></i>Availability
|
||||
</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Status:</span>
|
||||
<span class="detail-value" t-esc="candidate.availability or 'Not specified'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Categories:</span>
|
||||
<div class="skills-list">
|
||||
<t t-foreach="candidate.categ_ids" t-as="category">
|
||||
<span class="category-badge animate__animated animate__fadeInUp"
|
||||
t-att-data-delay="category_index * 50"
|
||||
t-esc="category.display_name"/>
|
||||
</t>
|
||||
<t t-if="not candidate.categ_ids">
|
||||
<span class="text-muted">None specified</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ats-card span-12" id="candidate-skills">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-star me-2"></i>Skills
|
||||
<small class="text-muted float-end">
|
||||
<span t-esc="len(candidate.candidate_skill_ids) or '0'"/>
|
||||
skills recorded
|
||||
</small>
|
||||
</h4>
|
||||
<div t-if="candidate.candidate_skill_ids and len(candidate.candidate_skill_ids) > 0"
|
||||
class="skills-container">
|
||||
<t t-foreach="candidate.candidate_skill_ids" t-as="skill">
|
||||
<div class="skill-item animate__animated animate__fadeInUp"
|
||||
t-att-data-delay="skill_index * 50">
|
||||
<div class="skill-info">
|
||||
<span class="skill-name" t-esc="skill.skill_id.display_name"/>
|
||||
<span class="skill-type" t-esc="skill.skill_type_id.display_name"/>
|
||||
</div>
|
||||
<div class="skill-level">
|
||||
<span class="skill-level-name"
|
||||
t-esc="skill.skill_level_id.display_name"/>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-animated" role="progressbar"
|
||||
t-att-style="'width: ' + str(skill.level_progress) + '%;'"
|
||||
t-att-aria-valuenow="skill.level_progress"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info animate__animated animate__fadeIn">No skills recorded
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="ats-card span-12" id="candidate-employment">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-briefcase me-2"></i>Employment History
|
||||
</h4>
|
||||
<div t-if="candidate.employer_history and len(candidate.employer_history) > 0"
|
||||
class="experience-list">
|
||||
<t t-foreach="candidate.employer_history" t-as="exp">
|
||||
<div class="experience-item animate__animated animate__fadeInLeft"
|
||||
t-att-data-delay="exp_index * 100">
|
||||
<div class="exp-header">
|
||||
<h5 t-esc="exp.designation or 'No designation'"/>
|
||||
<span class="exp-company" t-esc="exp.company_name or 'No company'"/>
|
||||
</div>
|
||||
<div class="exp-duration">
|
||||
<i class="fa fa-calendar me-1"></i>
|
||||
<span t-esc="exp.date_of_joining or 'Start date not specified'"/>
|
||||
<span>to</span>
|
||||
<span t-esc="exp.last_working_day or 'Present'"/>
|
||||
</div>
|
||||
<div class="exp-ctc" t-if="exp.ctc">
|
||||
<i class="fa fa-money-bill-wave me-1"></i>
|
||||
<span t-esc="exp.ctc"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info animate__animated animate__fadeIn">No employment history recorded
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="ats-card span-12" id="candidate-education">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-graduation-cap me-2"></i>Education History
|
||||
</h4>
|
||||
<div t-if="candidate.education_history and len(candidate.education_history) > 0"
|
||||
class="education-list">
|
||||
<t t-foreach="candidate.education_history" t-as="edu">
|
||||
<div class="education-item animate__animated animate__fadeInRight"
|
||||
t-att-data-delay="edu_index * 100">
|
||||
<div class="edu-header">
|
||||
<h5 t-esc="edu.name or 'No degree'"/>
|
||||
<span class="edu-type" t-esc="edu.education_type or 'No type'"/>
|
||||
</div>
|
||||
<div class="edu-university" t-esc="edu.university or 'No university'"/>
|
||||
<div class="edu-duration">
|
||||
<i class="fa fa-calendar me-1"></i>
|
||||
<span t-esc="edu.start_year or 'Start year not specified'"/>
|
||||
<span>to</span>
|
||||
<span t-esc="edu.end_year or 'Present'"/>
|
||||
</div>
|
||||
<div class="edu-marks" t-if="edu.marks_or_grade">
|
||||
<i class="fa fa-award me-1"></i>
|
||||
<span t-esc="edu.marks_or_grade"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info animate__animated animate__fadeIn">No education history recorded
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="ats-card span-12" id="candidate-family">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-users me-2"></i>Family Details
|
||||
</h4>
|
||||
<div t-if="candidate.family_details and len(candidate.family_details) > 0"
|
||||
class="family-list">
|
||||
<t t-foreach="candidate.family_details" t-as="member">
|
||||
<div class="family-item animate__animated animate__fadeInUp"
|
||||
t-att-data-delay="member_index * 50">
|
||||
<div class="family-header">
|
||||
<h5 t-esc="member.name or 'No name'"/>
|
||||
<span class="family-relation" t-esc="member.relation_type or 'No relation specified'"/>
|
||||
</div>
|
||||
<div class="family-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Contact:</span>
|
||||
<span class="detail-value" t-esc="member.contact_no or 'N/A'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Date of Birth:</span>
|
||||
<span class="detail-value" t-esc="member.dob or 'N/A'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Location:</span>
|
||||
<span class="detail-value" t-esc="member.location or 'N/A'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info animate__animated animate__fadeIn">No family details recorded
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="candidate_form_template">
|
||||
<div id="candidate-form-modal" class="candidate-form-modal">
|
||||
<div class="candidate-form-content">
|
||||
<div class="candidate-form-header">
|
||||
<div class="header-icon-container">
|
||||
<i class="fas fa-user-tie header-icon"></i>
|
||||
<h3>Candidate Profile</h3>
|
||||
</div>
|
||||
<span class="candidate-form-close">&times;</span>
|
||||
</div>
|
||||
|
||||
<div class="candidate-form-body">
|
||||
<form id="candidate-form" class="candidate-form">
|
||||
<!-- Header Section with Avatar and Basic Info -->
|
||||
<div class="form-section header-section">
|
||||
<div class="avatar-container">
|
||||
<div class="candidate-avatar">
|
||||
<img id="candidate-image" src="/web/static/src/img/placeholder.png"
|
||||
alt="Candidate Avatar"/>
|
||||
<div class="avatar-upload">
|
||||
<i class="fas fa-camera"></i>
|
||||
<input type="file" id="avatar-upload" accept="image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="basic-info">
|
||||
<div class="form-group">
|
||||
<label for="candidate-sequence">Candidate ID</label>
|
||||
<input type="text" id="candidate-sequence" class="form-input" readonly="1"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="partner-name">Full Name*</label>
|
||||
<input type="text" id="partner-name" class="form-input" required="1"
|
||||
placeholder="Candidate's Name"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="form-section contact-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-address-book"></i>
|
||||
Contact Information
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="email">Email*</label>
|
||||
<input type="email" id="email" class="form-input" required="1"
|
||||
placeholder="Email address"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone*</label>
|
||||
<input type="tel" id="phone" class="form-input" required="1"
|
||||
placeholder="Phone number"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="alternate-phone">Alternate Phone</label>
|
||||
<input type="tel" id="alternate-phone" class="form-input"
|
||||
placeholder="Alternate phone"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="linkedin">LinkedIn Profile</label>
|
||||
<input type="url" id="linkedin" class="form-input" placeholder="LinkedIn URL"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<t t-set="type_ids" t-value="request.env['hr.recruitment.degree'].search([])"/>
|
||||
<label for="type">Degree</label>
|
||||
<select id="type" class="form-select"
|
||||
data-placeholder="Select Degree" draggable="true">
|
||||
<option value="">Select type</option>
|
||||
<t t-foreach="type_ids" t-as="type_id" t-key="type_id.id">
|
||||
<option t-att-value="type_id.id">
|
||||
<t t-esc="type_id.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<t t-set="recruitment_users" t-value="request.env['res.users'].sudo().search([('groups_id', 'in', request.env.ref('hr_recruitment.group_hr_recruitment_manager').id)])"/>
|
||||
<label for="manager">Manager</label>
|
||||
<select id="manager" class="form-select select2"
|
||||
data-placeholder="Select Manager">
|
||||
<option value="">Select Manager</option>
|
||||
<t t-foreach="recruitment_users" t-as="user_id" t-key="user_id.id">
|
||||
<option t-att-value="user_id.id"
|
||||
t-att-data-image="'/web/image/res.users/' + str(user_id.id) + '/image_128'">
|
||||
<t t-esc="user_id.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="availability">Availability</label>
|
||||
<input type="date" id="availability" class="form-input" placeholder="Availability"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Section -->
|
||||
<div class="form-section skills-container" id="skills-container">
|
||||
<t t-set="skills" t-value="request.env['hr.skill'].search([])"/>
|
||||
|
||||
<label>Skills</label>
|
||||
<select id="candidate-skills" class="form-select select2" multiple="multiple"
|
||||
data-placeholder="Select skills" draggable="true">
|
||||
<t t-foreach="skills" t-as="skill" t-key="skill.id">
|
||||
<option t-att-value="skill.id">
|
||||
<t t-esc="skill.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Resume Section -->
|
||||
<div class="form-section resume-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
Resume
|
||||
</h4>
|
||||
<div class="resume-upload-container">
|
||||
<div class="resume-upload-area" id="resume-dropzone">
|
||||
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
||||
<h5>Upload Resume</h5>
|
||||
<p>Drag & drop your resume here or click to browse</p>
|
||||
<input type="file" id="resume-upload"
|
||||
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"/>
|
||||
</div>
|
||||
<div class="resume-preview" id="resume-preview">
|
||||
<div class="resume-preview-placeholder">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<p>Resume preview will appear here</p>
|
||||
</div>
|
||||
<iframe id="resume-iframe" style="display: none;"></iframe>
|
||||
<img id="resume-image" style="display: none; max-width: 100%; max-height: 400px;"/>
|
||||
<div id="unsupported-format" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p>Preview not available for this file type</p>
|
||||
<a id="download-resume" href="#" class="btn btn-primary mt-2">
|
||||
<i class="fas fa-download"></i>
|
||||
Download File
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-candidate-primary" id="upload-applicant-resume">
|
||||
<i class="fas fa-upload"></i>
|
||||
Upload Resume
|
||||
</button>
|
||||
<div class="footer-right">
|
||||
<button type="button" class="btn-cancel">Cancel</button>
|
||||
<button type="button" class="btn-candidate-primary btn-submit" id="save-candidate">
|
||||
<i class="fas fa-save"></i>
|
||||
Save Candidate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_candidate_view_tree_inherit_upload_doc" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.list.inherit.upload.doc</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="js_class">button_in_tree</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,864 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<!-- Job List Partial -->
|
||||
<template id="job_list_partial_page">
|
||||
<div class="ats-list-container">
|
||||
<!-- Search -->
|
||||
<!-- Dynamic Job Stats Header -->
|
||||
<!-- Job List and Details -->
|
||||
<div class="ats-list-body">
|
||||
<!-- Job List Panel -->
|
||||
<div class="ats-list-left expanded" id="job-list-panel">
|
||||
<div class="ats-list-search">
|
||||
<input type="text" id="ats-search" placeholder="🔍 Search Jobs..."/>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Button -->
|
||||
|
||||
<button id="job-list-sidebar-toggle-btn" class="ats-list-toggle-btn">☰</button>
|
||||
|
||||
<!-- Action Buttons Header -->
|
||||
<div class="ats-actions-header">
|
||||
<button class="btn" type="button" id="activeRecords" style="left:0;">
|
||||
Active Records (
|
||||
<span id="active-records-count">
|
||||
<t t-esc="len(jobs)"/>
|
||||
</span>
|
||||
)
|
||||
</button>
|
||||
<button class="btn add-create-btn" type="button" id="add-jd-create-btn" style="right:0;">
|
||||
<span class="plus-icon">+</span>
|
||||
Add New JD
|
||||
</button>
|
||||
</div>
|
||||
<div class="job-stats-header" id="job-stats-header">
|
||||
<div class="job-stats-values">
|
||||
<span class="badge badge-primary stat-toggle active" data-type="recruit">To Submit
|
||||
</span>
|
||||
<span class="badge badge-warning stat-toggle active" data-type="submitted">Submitted
|
||||
</span>
|
||||
<span class="badge badge-success stat-toggle active" data-type="hired">Hired</span>
|
||||
<span class="badge badge-danger stat-toggle active" data-type="rejected">Rejected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="ats-list">
|
||||
<t t-foreach="jobs" t-as="job">
|
||||
<li class="job-item ats-item"
|
||||
t-att-data-id="job.id"
|
||||
t-att-data-recruit="str(job.no_of_eligible_submissions or 0)"
|
||||
t-att-data-submitted="str(job.no_of_submissions or 0)"
|
||||
t-att-data-hired="str(job.no_of_hired_employee or 0)"
|
||||
t-att-data-rejected="str(job.no_of_refused_submissions or 0)">
|
||||
|
||||
<div class="ats-title">
|
||||
<strong>
|
||||
<t t-if="job.display_name">
|
||||
<t t-esc="job.display_name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="job.job_id.display_name"/>
|
||||
</t>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div class="ats-meta">
|
||||
<span>
|
||||
<t t-esc="job.job_category.display_name"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ats-badges">
|
||||
<span class="badge badge-primary job-badge" data-type="recruit">
|
||||
<t t-esc="job.no_of_eligible_submissions or 0"/>
|
||||
</span>
|
||||
<span class="badge badge-warning job-badge" data-type="submitted">
|
||||
<t t-esc="job.no_of_submissions or 0"/>
|
||||
</span>
|
||||
<span class="badge badge-success job-badge" data-type="hired">
|
||||
<t t-esc="job.no_of_hired_employee or 0"/>
|
||||
</span>
|
||||
<span class="badge badge-danger job-badge" data-type="rejected">
|
||||
<t t-esc="job.no_of_refused_submissions or 0"/>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Job Detail Panel -->
|
||||
<div id="job-detail" class="ats-detail">
|
||||
<em>Select a job to view details.</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-call="hr_recruitment_web_app.add_jd_modal_template"/>
|
||||
<t t-call="hr_recruitment_web_app.matching_candidates_popup"/>
|
||||
<t t-call="hr_recruitment_web_app.applicants_popup"/>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<template id="add_jd_modal_template">
|
||||
<div id="add-jd-modal" class="new-jd-container jd-modal">
|
||||
<div class="jd-modal-content">
|
||||
<div class="jd-modal-header">
|
||||
<div class="header-content">
|
||||
<i class="fas fa-file-alt header-icon"></i>
|
||||
<h3>Create New Job Description</h3>
|
||||
</div>
|
||||
<span class="jd-modal-close">&times;</span>
|
||||
</div>
|
||||
|
||||
<div class="jd-modal-body">
|
||||
<form id="jd-form" class="jd-creation-form">
|
||||
<!-- Job Position and ID Section -->
|
||||
<div class="form-section profile-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-id-card"></i>
|
||||
Job Identification
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="job-sequence">Unique ID</label>
|
||||
<input type="text" id="job-sequence" class="form-input"
|
||||
placeholder="Unique ID" required=""/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="job-position">Job Position</label>
|
||||
<select id="job-position" class="form-select select2" required="" data-placeholder="Select Job Position">
|
||||
<t t-set="all_jobs" t-value="request.env['hr.job'].search([])"/>
|
||||
<option value="">Select Job Position</option>
|
||||
<t t-foreach="all_jobs" t-as="job">
|
||||
<option t-att-value="job.id">
|
||||
<t t-esc="job.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1: Basic Information -->
|
||||
<div class="form-section profile-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Basic Information
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="job-category">Category</label>
|
||||
<select id="job-category" class="form-select" required="">
|
||||
<t t-set="categories" t-value="request.env['job.category'].search([])"/>
|
||||
<t t-foreach="categories" t-as="category">
|
||||
<option t-att-value="category.id">
|
||||
<t t-esc="category.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-priority">Priority</label>
|
||||
<select id="job-priority" class="form-select" required="">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Work Type</label>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input class="form-check-input" type="radio" name="work-type"
|
||||
id="work-type-internal" value="internal" checked=""/>
|
||||
<label class="form-check-label" for="work-type-internal">
|
||||
In-House
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input class="form-check-input" type="radio" name="work-type"
|
||||
id="work-type-external" value="external"/>
|
||||
<label class="form-check-label" for="work-type-external">
|
||||
Client-Side
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="client-session">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="client-company">Client Company</label>
|
||||
<select id="client-company" class="form-select select2" required=""
|
||||
data-placeholder="Select Client Company">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="client-id">Client</label>
|
||||
<select id="client-id" class="form-select select2" required=""
|
||||
data-placeholder="Select Client">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Employment Details -->
|
||||
<div class="form-section personal-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-file-signature"></i>
|
||||
Employment Details
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="employment-type">Employment Type</label>
|
||||
<select id="employment-type" class="form-select" required="">
|
||||
<t t-set="emp_types" t-value="request.env['hr.contract.type'].search([])"/>
|
||||
<t t-foreach="emp_types" t-as="emp_type">
|
||||
<option t-att-value="emp_type.id">
|
||||
<t t-esc="emp_type.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-budget">Budget</label>
|
||||
<input id="job-budget" class="form-input" placeholder="Enter budget"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-no-of-positions">Positions</label>
|
||||
<input id="job-no-of-positions" class="form-input" type="number" min="0"
|
||||
placeholder="Number of positions" required=""/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-eligible-submissions">Eligible Submissions</label>
|
||||
<input id="job-eligible-submissions" class="form-input" type="number" min="0"
|
||||
placeholder="Eligible Submissions" required=""/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="target-from">Target From</label>
|
||||
<input type="date" id="target-from" class="form-input"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="target-to">Target To</label>
|
||||
<input type="date" id="target-to" class="form-input"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Skills & Requirements -->
|
||||
<div class="form-section professional-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-tasks"></i>
|
||||
Skills & Requirements
|
||||
</h4>
|
||||
<t t-set="skills" t-value="request.env['hr.skill'].search([])"/>
|
||||
|
||||
<div class="form-grid" id="skill_requirements">
|
||||
<div class="form-group">
|
||||
<label for="job-primary-skills">Primary Skills</label>
|
||||
<select id="job-primary-skills" class="form-select select2" multiple="multiple"
|
||||
data-placeholder="Select primary skills" draggable="true">
|
||||
<t t-foreach="skills" t-as="skill" t-key="skill.id">
|
||||
<option t-att-value="skill.id">
|
||||
<t t-esc="skill.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-secondary-skills">Secondary Skills</label>
|
||||
<select id="job-secondary-skills" class="form-select select2" multiple="multiple"
|
||||
data-placeholder="Select secondary skills" draggable="true">
|
||||
<t t-foreach="skills" t-as="skill" t-key="skill.id">
|
||||
<option t-att-value="skill.id">
|
||||
<t t-esc="skill.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="job-experience">Experience</label>
|
||||
<select id="job-experience" class="form-select">
|
||||
<t t-set="job_experience" t-value="request.env['candidate.experience'].search([])"/>
|
||||
<t t-foreach="job_experience" t-as="experience">
|
||||
<option t-att-value="experience.id">
|
||||
<t t-esc="experience.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Recruitment Team -->
|
||||
<div class="form-section address-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-users-cog"></i>
|
||||
Recruitment Team
|
||||
</h4>
|
||||
<div class="recruitment-team">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="primary-recruiter">Primary Recruiter</label>
|
||||
<select id="primary-recruiter" class="form-select select2"
|
||||
data-placeholder="Choose recruiter">
|
||||
<t t-set="user_ids"
|
||||
t-value="request.env['res.users'].sudo().search([])"/>
|
||||
<t t-foreach="user_ids" t-as="user">
|
||||
<option t-att-value="user.id"
|
||||
t-att-data-image="'/web/image/res.users/' + str(user.id) + '/image_128'">
|
||||
<t t-esc="user.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="secondary-recruiter">Secondary Recruiter</label>
|
||||
<select id="secondary-recruiter" class="form-select select2"
|
||||
multiple="multiple" data-placeholder="Select recruiters">
|
||||
<t t-set="user_ids" t-value="request.env['res.users'].sudo().search([])"/>
|
||||
<t t-foreach="user_ids" t-as="user">
|
||||
<option t-att-value="user.id"
|
||||
t-att-data-image="'/web/image/res.users/' + str(user.id) + '/image_128'">
|
||||
<t t-esc="user.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Additional Information -->
|
||||
<div class="form-section jd-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Additional Information
|
||||
</h4>
|
||||
<div class="additional-info">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="job-locations">Locations</label>
|
||||
<select id="job-locations" class="form-select select2" multiple="multiple"
|
||||
data-placeholder="Select job locations">
|
||||
<t t-set="location_ids" t-value="request.env['hr.location'].search([])"/>
|
||||
<t t-foreach="location_ids" t-as="location">
|
||||
<option t-att-value="location.id">
|
||||
<t t-esc="location.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recruitment-stages">Recruitment Stages</label>
|
||||
<select id="recruitment-stages" class="form-select select2" multiple="multiple"
|
||||
data-placeholder="Select stages">
|
||||
<t t-set="recruitment_stages"
|
||||
t-value="request.env['hr.recruitment.stage'].search([])"/>
|
||||
<t t-foreach="recruitment_stages" t-as="stage">
|
||||
<option t-att-value="stage.id"
|
||||
t-att-selected="'selected' if stage.is_default_field else None">
|
||||
<t t-esc="stage.display_name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Description Section -->
|
||||
<div class="form-section description-section">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-align-left"></i>
|
||||
Job Description
|
||||
</h4>
|
||||
<div class="form-group full-width">
|
||||
<div id="job-description-editor" class="oe_editor"
|
||||
style="min-height: 300px; border: 1px solid #ddd; padding: 8px;"></div>
|
||||
<textarea id="job-description" name="description" style="display:none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form> </div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-jd-primary" id="upload-jd">
|
||||
<i class="fas fa-upload"></i>
|
||||
Upload JD
|
||||
</button>
|
||||
<div class="footer-right">
|
||||
<button type="button" class="btn-cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn-jd-primary" id="save-jd">
|
||||
<i class="fas fa-save"></i>
|
||||
Save Job Description
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Job Detail Partial - Web JD Version -->
|
||||
<template id="job_detail_partial">
|
||||
<div class="ats-grid job-detail-container" id="ats-details-container" t-att-data-job-id="job.id">
|
||||
<!-- Close button -->
|
||||
<button type="button" class="close-detail" aria-label="Close">
|
||||
<span aria-hidden="true">&times;</span>
|
||||
</button>
|
||||
|
||||
<!-- Smart Button -->
|
||||
<div class="smart-button-container">
|
||||
<button class="btn btn-primary smart-button" data-popup-type="matchingCandidates" data-popup-title="Matching Candidates" id="matching-jd-candidates">
|
||||
<i class="fa fa-magic me-1"></i> Matching Candidates
|
||||
</button>
|
||||
<button class="btn btn-primary smart-button" data-popup-type="applicants" data-popup-title="Applicants" id="jd-applicants">
|
||||
<i class="fa fa-magic me-1"></i> Applicants
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Published Ribbon -->
|
||||
<div t-att-class="'status-ribbon ribbon-' + ('published' if job.website_published else 'not-published')">
|
||||
<t t-esc="'Published' if job.website_published else 'Not Published'"/>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Sidebar -->
|
||||
<div class="ats-card span-3" id="ats-overview">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-list-ul me-2"></i>Quick Nav
|
||||
</h4>
|
||||
<div class="overview-nav">
|
||||
<ul class="nav-list">
|
||||
<li>
|
||||
<a href="#ats-header" class="nav-link smooth-scroll"><i class="fa fa-info-circle me-2"></i>Job Overview</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ats-basic-info" class="nav-link smooth-scroll"><i class="fa fa-info me-2"></i>Basic Information</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ats-requirements" class="nav-link smooth-scroll"><i class="fa fa-tasks me-2"></i>Requirements</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ats-request-info" class="nav-link smooth-scroll"><i class="fa fa-user-tie me-2"></i>Request Information</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ats-team" class="nav-link smooth-scroll"><i class="fa fa-users me-2"></i>Recruitment Team</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ats-description" class="nav-link smooth-scroll"><i class="fa fa-file-text me-2"></i>Job Description</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Header Section -->
|
||||
<div class="ats-card span-9" id="ats-header">
|
||||
<h2 class="ats-title">
|
||||
<span t-esc="job.job_id.display_name"/>
|
||||
</h2>
|
||||
<div class="ats-meta">
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-briefcase me-1"></i>
|
||||
<span t-esc="job.job_category.display_name"/>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-map-marker me-1"></i>
|
||||
<span t-if="job.address_id" t-esc="job.address_id.display_name"/>
|
||||
<span t-else="">Remote</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-clock-o me-1"></i>
|
||||
<t t-if="job.recruitment_type == 'internal'">In-House</t>
|
||||
<t t-elif="job.recruitment_type == 'external'">Client-Side</t>
|
||||
<t t-else="">Unknown</t>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="fa fa-star me-1"></i>
|
||||
<span t-esc="job.job_priority"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div class="recruitment-status">
|
||||
<span class="status-label">Recruitment Status:</span>
|
||||
<span class="status-value" t-esc="job.recruitment_status"/>
|
||||
</div>
|
||||
<div class="recruitment-progress">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
t-att-style="'width: ' + str(int(progress)) + '%;'"
|
||||
t-att-aria-valuenow="progress"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
<t t-esc="str(int(progress)) + '%'"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<small class="text-muted">
|
||||
<t t-esc="job.no_of_hired_employee or 0"/>
|
||||
of
|
||||
<t t-esc="job.no_of_recruitment or 0"/>
|
||||
positions filled
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<div class="ats-card span-6" id="ats-basic-info">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-info-circle me-2"></i>Basic Information
|
||||
</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Employment Type:</span>
|
||||
<span class="detail-value" t-esc="job.contract_type_id.display_name or 'N/A'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Budget:</span>
|
||||
<span class="detail-value" t-esc="job.budget or 'Not specified'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Target From:</span>
|
||||
<span class="detail-value" t-esc="job.target_from or 'N/A'"/>
|
||||
<t t-if="job.target_to">
|
||||
-
|
||||
<span class="detail-value" t-esc="job.target_to or 'N/A'"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Number of Positions:</span>
|
||||
<span class="detail-value" t-esc="job.no_of_recruitment or 'Not specified'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Eligible Submissions:</span>
|
||||
<span class="detail-value" t-esc="job.no_of_eligible_submissions or 'Not specified'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requirements Section -->
|
||||
<div class="ats-card span-6" id="ats-requirements">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-tasks me-2"></i>Requirements
|
||||
</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Experience:</span>
|
||||
<span class="detail-value" t-esc="job.experience.display_name or 'Not specified'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Primary Skills:</span>
|
||||
<div class="skills-list">
|
||||
<t t-foreach="job.skill_ids" t-as="skill">
|
||||
<span class="skill-badge" t-esc="skill.display_name"/>
|
||||
</t>
|
||||
<t t-if="not job.skill_ids">Not specified</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Secondary Skills:</span>
|
||||
<div class="skills-list">
|
||||
<t t-foreach="job.secondary_skill_ids" t-as="skill">
|
||||
<span class="skill-badge" t-esc="skill.display_name"/>
|
||||
</t>
|
||||
<t t-if="not job.secondary_skill_ids">Not specified</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Locations:</span>
|
||||
<div class="skills-list">
|
||||
<t t-foreach="job.locations" t-as="location">
|
||||
<span class="location-badge" t-esc="location.display_name"/>
|
||||
</t>
|
||||
<t t-if="not job.locations">Not specified</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Information Section -->
|
||||
<div class="ats-card span-6" id="ats-request-info">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-user-tie me-2"></i>Request Information
|
||||
</h4>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Requested By:</span>
|
||||
<span class="detail-value" t-esc="job.requested_by.display_name or 'N/A'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Department:</span>
|
||||
<span class="detail-value" t-esc="job.department_id.display_name or 'N/A'"/>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Company:</span>
|
||||
<span class="detail-value" t-esc="job.address_id.display_name or 'N/A'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recruitment Team Section -->
|
||||
<div class="ats-card span-6" id="ats-team">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-users me-2"></i>Recruitment Team
|
||||
</h4>
|
||||
<div class="team-member">
|
||||
<div class="member-header">
|
||||
<i class="fa fa-user-circle me-2"></i>
|
||||
<span class="member-title">Primary Recruiter</span>
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<t t-if="job.user_id">
|
||||
<div class="recruiter-inline d-flex align-items-center">
|
||||
<img t-att-src="'/web/image/res.users/' + str(job.user_id.id) + '/image_128'"
|
||||
class="rounded-circle recruiter-photo me-3"
|
||||
alt="Recruiter Photo"/>
|
||||
<div>
|
||||
<div class="member-name" t-esc="job.user_id.display_name"/>
|
||||
<div class="member-email">
|
||||
<i class="fa fa-envelope me-1"></i>
|
||||
<span t-esc="job.user_id.email"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-warning">No primary recruiter assigned</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-members">
|
||||
<div class="member-header mb-3">
|
||||
<i class="fa fa-users me-2"></i>
|
||||
<span class="member-title">Secondary Recruiters</span>
|
||||
</div>
|
||||
<t t-if="job.interviewer_ids">
|
||||
<div class="row row-cols-1 row-cols-md-2 g-4">
|
||||
<t t-foreach="job.interviewer_ids" t-as="interviewer">
|
||||
<div class="col">
|
||||
<div class="team-member recruiter-inline">
|
||||
<img t-att-src="'/web/image/res.users/' + str(interviewer.id) + '/image_128'"
|
||||
class="rounded-circle recruiter-photo"
|
||||
alt="Recruiter Photo"/>
|
||||
<div>
|
||||
<div class="member-name" t-esc="interviewer.display_name"/>
|
||||
<div class="member-email">
|
||||
<i class="fa fa-envelope me-1"></i>
|
||||
<span t-esc="interviewer.email"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info">No secondary recruiters assigned</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Description Section -->
|
||||
<div class="ats-card span-12" id="ats-description">
|
||||
<h4 class="section-title">
|
||||
<i class="fa fa-file-text me-2"></i>Job Description
|
||||
</h4>
|
||||
<div class="description-content">
|
||||
<t t-if="job.description">
|
||||
<t t-raw="job.description"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-info">No description provided for this job.</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="matching_candidates_popup">
|
||||
<div class="popup-container bottom-right" id="matchingCandidatesPopup">
|
||||
<div class="popup-header">
|
||||
<h5>Matching Candidates</h5>
|
||||
<button type="button" class="popup-close">&times;</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div class="popup-content" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="applicants_popup">
|
||||
<div class="popup-container bottom-right" id="applicantsPopup">
|
||||
<div class="popup-header">
|
||||
<h5>Applicants</h5>
|
||||
<button type="button" class="popup-close">&times;</button>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div class="popup-content" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<!-- Matching Candidates Content Template -->
|
||||
<template id="matching_candidates_content">
|
||||
<div class="matching-candidates-container">
|
||||
<!-- Search Box Only -->
|
||||
<div class="mc-search-box">
|
||||
<input type="text" id="mc-search" placeholder="Search candidates..." class="mc-search-input"/>
|
||||
<span class="mc-search-icon">🔍</span>
|
||||
<span class="mc-match-threshold">
|
||||
Showing matches ≥ <t t-esc="min_match"/>%
|
||||
</span>
|
||||
</div>
|
||||
<div class="mc-match-threshold" style="padding: 10px; text-align: center;">
|
||||
Active Records (<t t-esc="len(candidates)"/>)
|
||||
</div>
|
||||
|
||||
<!-- Candidates Grid -->
|
||||
<div class="mc-cards-container">
|
||||
<t t-foreach="candidates" t-as="candidate">
|
||||
<div class="mc-card"
|
||||
t-att-data-id="candidate.id"
|
||||
t-att-data-candidate = "candidate.display_name"
|
||||
t-att-data-image = "candidate.candidate_image"
|
||||
t-att-data-email = "match_data[candidate.id]['candidate_data']['email']"
|
||||
t-att-data-phone = "match_data[candidate.id]['candidate_data']['phone']"
|
||||
t-att-data-manager = "match_data[candidate.id]['candidate_data']['manager']"
|
||||
t-att-data-applications = "match_data[candidate.id]['candidate_data']['applications']"
|
||||
t-att-data-primary-percent = "match_data[candidate.id]['primary_pct']"
|
||||
t-att-data-secondary-percent = "match_data[candidate.id]['secondary_pct']"
|
||||
t-att-data-match-primary-skills = "match_data[candidate.id]['matched_primary_skills']"
|
||||
t-att-data-match-secondary-skills = "match_data[candidate.id]['matched_secondary_skills']"
|
||||
t-on-click="onCandidateClick">
|
||||
<!-- Candidate Avatar with Percentage Circles -->
|
||||
<div class="mc-avatar-wrapper">
|
||||
<div class="mc-avatar">
|
||||
<t t-if="candidate.candidate_image">
|
||||
<img t-att-src="image_data_uri(candidate.candidate_image)"
|
||||
class="mc-avatar-img" alt="Candidate"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="mc-avatar-initials">
|
||||
<t t-esc="candidate.display_name[0].upper() if candidate.display_name else '?'"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<h3 class="mc-detail-name">
|
||||
<t t-esc="candidate.display_name or 'Unnamed Candidate'"/>
|
||||
</h3>
|
||||
|
||||
|
||||
<!-- Percentage Circles -->
|
||||
<div class="mc-percentage-circles">
|
||||
<div class="mc-percentage-circle mc-primary-circle"
|
||||
t-att-data-percent="match_data[candidate.id]['primary_pct']">
|
||||
<span class="mc-percentage-value">
|
||||
<t t-esc="int(match_data[candidate.id]['primary_pct'])"/>%
|
||||
</span>
|
||||
<span class="mc-percentage-label">Primary</span>
|
||||
</div>
|
||||
<div class="mc-percentage-circle mc-secondary-circle"
|
||||
t-att-data-percent="match_data[candidate.id]['secondary_pct']">
|
||||
<span class="mc-percentage-value">
|
||||
<t t-esc="int(match_data[candidate.id]['secondary_pct'])"/>%
|
||||
</span>
|
||||
<span class="mc-percentage-label">Secondary</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<t t-if="not candidates">
|
||||
<div class="mc-empty-state">
|
||||
<div class="mc-empty-icon">😕</div>
|
||||
<h4>No matching candidates found</h4>
|
||||
<p>Try lowering the match threshold or expanding your search criteria</p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Candidate Detail Modal (Hidden by default) -->
|
||||
<div id="candidateDetailModal" class="mc-modal">
|
||||
<div class="mc-modal-content">
|
||||
<span class="mc-close-modal">&times;</span>
|
||||
<div id="candidateDetailContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Applicants Content Template -->
|
||||
<template id="applicants_content">
|
||||
<div class="applicants-container">
|
||||
<h6>Applicants for <t t-esc="job_title"/> (<t t-esc="total_applicants"/>)</h6>
|
||||
|
||||
<div class="stages-container">
|
||||
<t t-foreach="stages" t-as="stage">
|
||||
<div class="stage-card mb-3">
|
||||
<h6><t t-esc="stage['name']"/> (<t t-esc="len(stage['applicants'])"/>)</h6>
|
||||
|
||||
<div class="applicant-list">
|
||||
<t t-foreach="stage['applicants']" t-as="applicant">
|
||||
<div class="applicant-card">
|
||||
<div class="applicant-info">
|
||||
<strong><t t-esc="applicant['name']"/></strong>
|
||||
<div class="text-muted small">
|
||||
Applied: <t t-esc="applicant['application_date']"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="applicant-actions">
|
||||
<a t-att-href="applicant['resume_url']"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
t-if="applicant['resume_url'] != '#'">
|
||||
View Resume
|
||||
</a>
|
||||
<span class="text-muted" t-if="applicant['resume_url'] == '#'">
|
||||
No Resume
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error Template -->
|
||||
<template id="error_template">
|
||||
<div class="alert alert-danger">
|
||||
<t t-esc="message"/>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,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><<</i>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="content-area">
|
||||
<div id="main-content"></div>
|
||||
<div id="job-detail"></div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="action_custom_webapp" model="ir.actions.act_url">
|
||||
<field name="name">My WebApp</field>
|
||||
<field name="type">ir.actions.act_url</field>
|
||||
<field name="url">/myATS</field>
|
||||
<field name="target">new</field> <!-- Opens in new tab -->
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_ats_webapp_root"
|
||||
name="My ATS Web App"
|
||||
web_icon="hr_recruitment_web_app,static/description/banner.png"
|
||||
sequence="10"
|
||||
action="action_custom_webapp"/>
|
||||
|
||||
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_recruitment_doc_upload_wizard" model="ir.ui.view">
|
||||
<field name="name">recruitment.doc.upload.wizard.form</field>
|
||||
<field name="model">recruitment.doc.upload.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Upload Resumes">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="file_name" invisible="0"/>
|
||||
<field name="file_data" widget="binary" filename="file_name"/>
|
||||
<field name="mimetype" readonly="1" invisible="0" force_save="1"/>
|
||||
<notebook>
|
||||
<page string="Data" name="json_data">
|
||||
<button name="action_fetch_json" type="object" class="btn-primary" string="Fetch Data"/>
|
||||
<field name="json_data"/>
|
||||
</page>
|
||||
<page string="HTML Text" name="html_text">
|
||||
<field name="file_html_text"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Create Candidate" type="object" name="action_upload" class="btn-primary"/>
|
||||
<button string="Cancel" type="object" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to trigger the wizard -->
|
||||
<record id="action_recruitment_doc_upload_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Upload Resumes</field>
|
||||
<field name="res_model">recruitment.doc.upload.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="view_recruitment_doc_upload_wizard"/>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_upload_resumes"
|
||||
name="Upload Resumes"
|
||||
sequence="3"
|
||||
parent="hr_recruitment.menu_hr_recruitment_root"
|
||||
action="action_recruitment_doc_upload_wizard"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<!-- <record id="res_config_settings_view_form_inherited_qwen" model="ir.ui.view">-->
|
||||
<!-- <field name="name">res.config.settings.view.form.inherit.account</field>-->
|
||||
<!-- <field name="model">res.config.settings</field>-->
|
||||
<!-- <field name="priority" eval="40"/>-->
|
||||
<!-- <field name="inherit_id" ref="hr_recruitment.res_config_settings_view_form"/>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <xpath expr="//block[@name='recruitment_in_app_purchases']" position="after">-->
|
||||
<!-- <h2>Qwen API</h2>-->
|
||||
<!-- <div class="row mt16 o_settings_container" name="performance">-->
|
||||
<!-- <div class="col-12 col-lg-6 o_setting_box" id="qwen_api_key">-->
|
||||
<!-- <label for="qwen_api_key"/>-->
|
||||
<!-- <field name="qwen_api_key"/>-->
|
||||
<!-- <div class="text-muted">-->
|
||||
<!-- Enter Your API key of the Together.ai.-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- </xpath>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
</odoo>
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import models
|
||||
from . import menu
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,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 });
|
||||
|
|
@ -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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import offer_letter
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 & 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 & 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 & 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 & 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 & 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 & 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) < 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>
|
||||
|
||||
|
|
@ -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,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'] < 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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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/**/*",
|
||||
],
|
||||
},
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||