diff --git a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js index 23b5c1ebb..9eab0c9e9 100644 --- a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js +++ b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js @@ -1,50 +1,64 @@ /** @odoo-module **/ -import { useEffect, useState, Component ,onMounted, useRef} from "@odoo/owl"; +import { Component, useEffect, useState, onMounted, useRef, onWillStart } from "@odoo/owl"; import { registry } from "@web/core/registry"; const actionRegistry = registry.category("actions"); import { useService } from "@web/core/utils/hooks"; import { ActivityMenu } from "@hr_attendance/components/attendance_menu/attendance_menu"; import { patch } from "@web/core/utils/patch"; - +import { loadJS } from "@web/core/assets"; export class NetflixProfileContainer extends Component { - static template = 'employee_profile_template'; // The template for the profile + static template = 'employee_profile_template'; static props = ["*"]; + setup() { - super.setup(...arguments); - debugger; this.orm = useService("orm"); this.effect = useService("effect"); - this.log_in_out = useRef("log_in_out") - this.action = useService("action"); this.action = useService("action"); + this.chart = null; - - // Initialize state for storing employee data this.state = useState({ login_employee: { name: '', image_1920: '', job_id: null, current_company_exp: null, - doj:'', - employee_id:null, + doj: '', + employee_id: null, birthday: '', - attendance_state:null, + attendance_state: null, mobile_phone: '', work_email: '', private_street: '', - department_id:'' + department_id: '' }, - attendance_lines:[], - leaves:[] + attendance_lines: [], + leaves: [] + }); - }); - onMounted(() => { - this.fetchEmployeeData(); - }); + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + + onMounted(async () => { + await this.fetchEmployeeData(); + + // Make sure Chart is loaded and canvas is available + await loadJS(["/web/static/lib/Chart/Chart.js"]); + if (typeof Chart === 'undefined') { + console.error("Chart.js failed to load"); + return; } + + const canvas = document.querySelector("#attendanceChart"); + if (!canvas) { + console.warn("Canvas element not found. Skipping chart render."); + return; + } + + +}); + } + hr_timesheets() { this.action.doAction({ name: "Timesheets", @@ -52,52 +66,131 @@ export class NetflixProfileContainer extends Component { res_model: 'account.analytic.line', view_mode: 'list,form', views: [[false, 'list'], [false, 'form']], - context: { - 'search_default_month': true, - }, - domain: [['employee_id.user_id','=', this.props.action.context.user_id]], + context: { 'search_default_month': true }, + domain: [['employee_id.user_id', '=', this.props.action.context.user_id]], target: 'current' - }) + }); } - attendance_sign_in_out() { - if (this.state.login_employee.attendance_state == 'checked_out') { - this.state.login_employee.attendance_state = 'checked_in' - } - else{ - if (this.state.login_employee.attendance_state == 'checked_in') { - this.state.login_employee.attendance_state = 'checked_out' - } - } - this.update_attendance() + + hr_payslip() { + this.action.doAction({ + name: "Employee Payslips", + type: 'ir.actions.act_window', + res_model: 'hr.payslip', + view_mode: 'list,form,calendar', + views: [[false, 'list'],[false, 'form']], + domain: [['employee_id','=', this.props.action.context.user_id]], + target: 'current' + }); } - async update_attendance() { - var self = this; - var result = await this.orm.call('hr.employee', 'attendance_manual',[[this.props.action.context.user_id]]) - if (result) { - var attendance_state = this.state.login_employee.attendance_state; - var message = '' - if (attendance_state == 'checked_in'){ - message = 'Checked In' - this.env.bus.trigger('signin_signout', { - mode: "checked_in", - }); - } - else if (attendance_state == 'checked_out'){ - message = 'Checked Out' - this.env.bus.trigger('signin_signout', { - mode: false, - }); - } - this.effect.add({ - message: ("Successfully " + message), - type: 'rainbow_man', - fadeout: "fast", + async hr_contract() { + if(this.isHrManager){ + this.action.doAction({ + name: _t("Contracts"), + type: 'ir.actions.act_window', + res_model: 'hr.contract', + view_mode: 'tree,form,calendar', + views: [[false, 'list'],[false, 'form']], + context: { + 'search_default_employee_id': this.props.action.context.user_id, + }, + target: 'current' }) } } - add_attendance() { + + renderChart() { + const labels = this.state.attendance_lines.map(r => r.create_date); + const data = this.state.attendance_lines.map(r => parseFloat(r.worked_hours)); + + const ctx = document.querySelector("#attendanceChart"); + if (!ctx) { + console.warn("Canvas context not found for chart."); + return; + } + + if (this.chart) { + this.chart.destroy(); + } + + this.chart = new Chart(ctx, { + type: "bar", // Base type + data: { + labels, + datasets: [ + { + type: 'bar', + label: "Hours Worked (Bar)", + data, + backgroundColor: "rgba(54, 162, 235, 0.6)", + borderColor: "rgba(54, 162, 235, 1)", + borderWidth: 1 + }, + { + type: 'line', + label: "Hours Worked (Line)", + data, + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgba(255, 99, 132, 1)", + borderWidth: 2, + tension: 0.3, + fill: false, + pointRadius: 4, + pointHoverRadius: 6 + } + ] + }, + options: { + responsive: true, + plugins: { + legend: { display: true }, + tooltip: { mode: 'index', intersect: false } + }, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: "Hours" + } + }, + x: { + title: { + display: true, + text: "Date" + }, + reverse: true + } + } + } + }); +} + + + async attendance_sign_in_out() { + const result = await this.orm.call('hr.employee', 'attendance_manual', [[this.props.action.context.user_id]]); + if (result) { + let current = this.state.login_employee.attendance_state; + this.state.login_employee.attendance_state = (current === 'checked_in') ? 'checked_out' : 'checked_in'; + + let mode = this.state.login_employee.attendance_state; + let message = mode === 'checked_in' ? 'Checked In' : 'Checked Out'; + + this.env.bus.trigger('signin_signout', { + mode: mode === 'checked_in' ? "checked_in" : false, + }); + + this.effect.add({ + message: "Successfully " + message, + type: 'rainbow_man', + fadeout: "fast" + }); + } + } + + add_attendance() { this.action.doAction({ - name: ("Attendances"), + name: "Attendances", type: 'ir.actions.act_window', res_model: 'hr.attendance', view_mode: 'form', @@ -105,9 +198,10 @@ export class NetflixProfileContainer extends Component { target: 'new' }); } + add_leave() { this.action.doAction({ - name: ("Leave Request"), + name: "Leave Request", type: 'ir.actions.act_window', res_model: 'hr.leave', view_mode: 'form', @@ -116,95 +210,147 @@ export class NetflixProfileContainer extends Component { }); } - - // Method to fetch employee data from hr.employee model async fetchEmployeeData() { - console.log(this.props.action.context.user_id) - try { + const userId = this.props.action.context.user_id; + const employeeData = await this.orm.call("hr.employee", 'get_user_employee_details'); + if (!employeeData.length) return; - const employeeData = await this.orm.call( - "hr.employee",'get_user_employee_details'); - const attendanceLines = await this.orm.searchRead( - 'hr.attendance', - [['employee_id', '=', employeeData[0].id]], - ['create_date', 'check_in', 'check_out', 'worked_hours']); - const Leaves = await this.orm.searchRead('hr.leave',[['employee_id', '=', employeeData[0].id]], - ['request_date_from', 'request_date_to', 'state','holiday_status_id']); + const employee = employeeData[0]; - Leaves.forEach(line => { - // Extract the 'type' from 'holiday_status_id' and assign it - line['type'] = line['holiday_status_id'][1]; + const now = new Date(); +const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); +const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); - // Change state and set color based on the state - if (line['state'] === 'confirm') { - line['state'] = 'To Approve'; - line['color'] = 'orange'; - } else if (line['state'] === 'validate1') { - line['state'] = 'Second Approval'; - line['color'] = '#7CFC00'; - } else if (line['state'] === 'validate') { - line['state'] = 'Approved'; - line['color'] = 'green'; - } else if (line['state'] === 'cancel') { - line['state'] = 'Cancelled'; - line['color'] = 'red'; - } else { - line['state'] = 'Refused'; - line['color'] = 'red'; - } - }); +// Format as Odoo-compatible strings: 'YYYY-MM-DD' +const startDateStr = startOfMonth.toISOString().split('T')[0]; +const endDateStr = endOfMonth.toISOString().split('T')[0]; +const attendanceLines = await this.orm.searchRead( + 'hr.attendance', + [ + ['employee_id', '=', employee.id], + ['create_date', '>=', startDateStr], + ['create_date', '<=', endDateStr] + ], + ['create_date', 'check_in', 'check_out', 'worked_hours'] +); - if (employeeData.length > 0) { - const employee = employeeData[0]; - attendanceLines.forEach(line => { - let createDate = new Date(line.create_date); - line.create_date = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' }); // Format as 'YYYY-MM-DD' - let checkIn = new Date(line.check_in + 'Z'); - line.check_in = checkIn.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM' - let checkOut = new Date(line.check_out + 'Z'); - line.check_out = checkOut.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM' - line.worked_hours = line.worked_hours.toFixed(2); - }); - this.state.attendance_lines = attendanceLines, - this.state.leaves = Leaves + const leaves = await this.orm.searchRead( + 'hr.leave', + [['employee_id', '=', employee.id]], + ['request_date_from', 'request_date_to', 'state', 'holiday_status_id'] + ); - this.state.login_employee = { - name: employee.name, - image_1920: employee.image_1920, - doj:employee.doj, - job_id: employee.job_id, - employee_id:employee.employee_id, - current_company_exp: employee.current_company_exp, - attendance_state:employee.attendance_state, - birthday: employee.birthday, - mobile_phone: employee.mobile_phone, - work_email: employee.work_email, - private_street: employee.private_street, - department_id:employee.department_id[1] - }; - } +const groupedLines = {}; + +attendanceLines.forEach(line => { + const createDate = new Date(line.create_date); + const dateStr = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' }); + + const workedHours = parseFloat(line.worked_hours); + + const checkIn = new Date(line.check_in + 'Z'); + const checkOut = new Date(line.check_out + 'Z'); + + const checkInStr = checkIn.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Kolkata' }); + const checkOutStr = checkOut.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Kolkata' }); + + if (!groupedLines[dateStr]) { + groupedLines[dateStr] = { + create_date: dateStr, + worked_hours: workedHours, + check_in: checkInStr, + check_out: checkOutStr, + earliestCheckIn: checkIn, + latestCheckOut: checkOut + }; + } else { + groupedLines[dateStr].worked_hours += workedHours; + + // Update earliest check-in + if (checkIn < groupedLines[dateStr].earliestCheckIn) { + groupedLines[dateStr].check_in = checkInStr; + groupedLines[dateStr].earliestCheckIn = checkIn; + } + + // Update latest check-out + if (checkOut > groupedLines[dateStr].latestCheckOut) { + groupedLines[dateStr].check_out = checkOutStr; + groupedLines[dateStr].latestCheckOut = checkOut; + } + } +}); + +// Format final result +const groupedAttendance = Object.values(groupedLines).map(line => ({ + create_date: line.create_date, + worked_hours: line.worked_hours.toFixed(2), + check_in: line.check_in, + check_out: line.check_out +})); + +// Assign to state +this.state.attendance_lines = groupedAttendance; + this.renderChart(); + + leaves.forEach(line => { + line.type = line.holiday_status_id[1]; + switch (line.state) { + case 'confirm': + line.state = 'To Approve'; + line.color = 'orange'; + break; + case 'validate1': + line.state = 'Second Approval'; + line.color = '#7CFC00'; + break; + case 'validate': + line.state = 'Approved'; + line.color = 'green'; + break; + case 'cancel': + line.state = 'Cancelled'; + line.color = 'red'; + break; + default: + line.state = 'Refused'; + line.color = 'red'; + break; + } + }); + + this.state.login_employee = { + name: employee.name, + image_1920: employee.image_1920, + doj: employee.doj, + job_id: employee.job_id, + employee_id: employee.employee_id, + current_company_exp: employee.current_company_exp, + attendance_state: employee.attendance_state, + birthday: employee.birthday, + mobile_phone: employee.mobile_phone, + work_email: employee.work_email, + private_street: employee.private_street, + department_id: employee.department_id[1] + }; + + this.state.leaves = leaves; } catch (error) { - console.error('Error fetching employee data:', error); + console.error("Error fetching employee data:", error); } } } -registry.category("actions").add("NetflixProfileContainer", NetflixProfileContainer) + +actionRegistry.add("NetflixProfileContainer", NetflixProfileContainer); +// Patch the attendance menu to update icon on signin/signout patch(ActivityMenu.prototype, { setup() { super.setup(); - var self = this onMounted(() => { - this.env.bus.addEventListener('signin_signout', ({ - detail - }) => { - if (detail.mode == 'checked_in') { - self.state.checkedIn = detail.mode - } else { - self.state.checkedIn = false - } - }) - }) - }, -}) + this.env.bus.addEventListener('signin_signout', ({ detail }) => { + this.state.checkedIn = detail.mode === "checked_in" ? true : false; + }); + }); + } +}); diff --git a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml index 8dfeec8d4..b3fbcc562 100644 --- a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml +++ b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml @@ -74,6 +74,7 @@
++ Payslips +
+