feature/odoo18 #2
|
|
@ -1,2 +1,2 @@
|
||||||
|
from . import controllers
|
||||||
from . import models
|
from . import models
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'summary': 'Display employee profile using Owl.js',
|
'summary': 'Display employee profile using Owl.js',
|
||||||
'depends': ['base', 'hr', 'web','muk_web_theme'], # Depends on base, hr, and web for Owl.js and hr.employee model
|
'depends': ['base', 'hr', 'web'], # Depends on base, hr, and web for Owl.js and hr.employee model
|
||||||
'data': [
|
'data': [
|
||||||
'views/employee_dashboard_views.xml', # Your template
|
'views/employee_dashboard_views.xml', # Your template
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import employee_dashboard
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
class EmployeeDashboard(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/employee/leave/balance', auth='user', type='json')
|
||||||
|
def get_leave_balance(self):
|
||||||
|
employee = request.env.user.employee_id
|
||||||
|
if not employee:
|
||||||
|
return {'error': 'No employee linked to this user'}
|
||||||
|
|
||||||
|
leave_data = {}
|
||||||
|
leave_types = request.env['hr.leave.type'].search([
|
||||||
|
])
|
||||||
|
if not leave_types:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for leave_type in leave_types:
|
||||||
|
allocations = request.env['hr.leave.allocation'].search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('holiday_status_id', '=', leave_type.id),
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
])
|
||||||
|
taken_leaves = request.env['hr.leave'].search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('holiday_status_id', '=', leave_type.id),
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
leave_data[leave_type.name] = {
|
||||||
|
'allocated': total_allocated,
|
||||||
|
'taken': total_taken,
|
||||||
|
'remaining': remaining,
|
||||||
|
}
|
||||||
|
|
||||||
|
return leave_data
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
import { rpc } from "@web/core/network/rpc";
|
||||||
import { Component, useEffect, useState, onMounted, useRef, onWillStart } from "@odoo/owl";
|
import { Component, useEffect, useState, onMounted, useRef, onWillStart } from "@odoo/owl";
|
||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
const actionRegistry = registry.category("actions");
|
const actionRegistry = registry.category("actions");
|
||||||
|
|
@ -34,13 +34,31 @@ export class NetflixProfileContainer extends Component {
|
||||||
department_id: ''
|
department_id: ''
|
||||||
},
|
},
|
||||||
attendance_lines: [],
|
attendance_lines: [],
|
||||||
leaves: []
|
leaves: [],
|
||||||
|
leave_allocations:[]
|
||||||
});
|
});
|
||||||
|
|
||||||
onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"]));
|
onWillStart(async () => {
|
||||||
|
await loadJS("/web/static/lib/Chart/Chart.js");
|
||||||
|
const result = await rpc("/employee/leave/balance");
|
||||||
|
const LEAVE_NAME_MAP = {
|
||||||
|
"Sick Time Off": "SL",
|
||||||
|
"Casual Leave": "CL",
|
||||||
|
"Privilege Leave": "PL"
|
||||||
|
};
|
||||||
|
const filteredLeaves = {};
|
||||||
|
for (const [name, data] of Object.entries(result)) {
|
||||||
|
const code = LEAVE_NAME_MAP[name];
|
||||||
|
if (code) {
|
||||||
|
filteredLeaves[code] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state.leave_allocations = filteredLeaves;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await this.fetchEmployeeData();
|
await this.fetchEmployeeData();
|
||||||
|
await this.drawChartWhenReady();
|
||||||
|
|
||||||
// Make sure Chart is loaded and canvas is available
|
// Make sure Chart is loaded and canvas is available
|
||||||
await loadJS(["/web/static/lib/Chart/Chart.js"]);
|
await loadJS(["/web/static/lib/Chart/Chart.js"]);
|
||||||
|
|
@ -58,7 +76,48 @@ export class NetflixProfileContainer extends Component {
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
drawChartWhenReady() {
|
||||||
|
// Wait for data to be loaded before drawing chart
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Object.keys(this.state.leave_allocations).length > 0 && window.Chart) {
|
||||||
|
this.drawChart();
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
drawChart() {
|
||||||
|
const ctx = document.getElementById("leaveChart")?.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn("leaveChart canvas not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaves = this.state.leave_allocations;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: ['Sick Leave (SL)', 'Casual Leave (CL)', 'Privilege Leave (PL)'],
|
||||||
|
datasets: [{
|
||||||
|
data: [
|
||||||
|
leaves.SL?.remaining || 0,
|
||||||
|
leaves.CL?.remaining || 0,
|
||||||
|
leaves.PL?.remaining || 0,
|
||||||
|
],
|
||||||
|
backgroundColor: ['#ed8e34', '#4787bf', '#ff0080'],
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { position: 'bottom' },
|
||||||
|
title: { display: true, text: 'Available Leave Balance' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
hr_timesheets() {
|
hr_timesheets() {
|
||||||
this.action.doAction({
|
this.action.doAction({
|
||||||
name: "Timesheets",
|
name: "Timesheets",
|
||||||
|
|
@ -144,7 +203,8 @@ export class NetflixProfileContainer extends Component {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true },
|
legend: { display: true },
|
||||||
tooltip: { mode: 'index', intersect: false }
|
tooltip: { mode: 'index', intersect: false },
|
||||||
|
title: { display: true, text: 'Attendance' }
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
|
|
|
||||||
|
|
@ -130,11 +130,55 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" >
|
<div class="container m-3" style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
|
||||||
<div style="width: 100%; max-width: 600px; display: block; margin-left: 0;">
|
|
||||||
<canvas id="attendanceChart" width="600" height="300"></canvas>
|
<!-- Attendance Chart Block -->
|
||||||
</div>
|
<div class="dashboard-card" style="
|
||||||
</div>
|
flex: 1 1 500px;
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
">
|
||||||
|
<h3>📊 Attendance Summary</h3>
|
||||||
|
<canvas id="attendanceChart" style="width: 100%; height: 350px;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leave Balance Card -->
|
||||||
|
<div class="dashboard-card" style="
|
||||||
|
flex: 1 1 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
">
|
||||||
|
<h3>🗓️ Leave Balance</h3>
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
|
||||||
|
<!-- Chart Column -->
|
||||||
|
<div style="flex: 1 1 200px; min-width: 150px;">
|
||||||
|
<canvas id="leaveChart" style="width: 100%; height: auto;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leave Summary Cards -->
|
||||||
|
<div style="flex: 1 1 200px; min-width: 150px;">
|
||||||
|
<ul style="display: flex; flex-direction: column; gap: 10px; padding-top:30px ; list-style: none; margin:0;">
|
||||||
|
<li style="padding: 12px 16px; background: #f5f5f5; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.1);">
|
||||||
|
<strong>Sick Leave (SL):</strong> <t t-esc="state.leave_allocations.SL?.remaining || 0"/> days
|
||||||
|
</li>
|
||||||
|
<li style="padding: 12px 16px; background: #f5f5f5; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.1);">
|
||||||
|
<strong>Casual Leave (CL):</strong> <t t-esc="state.leave_allocations.CL?.remaining || 0"/> days
|
||||||
|
</li>
|
||||||
|
<li style="padding: 12px 16px; background: #f5f5f5; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,0.1);">
|
||||||
|
<strong>Privilege Leave (PL):</strong> <t t-esc="state.leave_allocations.PL?.remaining || 0"/> days
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<div class="tables-section">
|
<div class="tables-section">
|
||||||
<!-- Attendance Table -->
|
<!-- Attendance Table -->
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue