first push message
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,400 @@
|
||||
/* Main Dashboard Container */
|
||||
.o_project_dashboard {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
overflow:scroll;
|
||||
height:100vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.dashboard-header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.greeting-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
border: 3px solid #007bff;
|
||||
}
|
||||
|
||||
.greeting-text h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.greeting-text p {
|
||||
margin: 5px 0 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-group select,
|
||||
.filter-group input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* KPI Cards */
|
||||
.kpi-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid #007bff;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.kpi-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.kpi-card.my-tasks { border-left-color: #007bff; }
|
||||
.kpi-card.my-overdue { border-left-color: #dc3545; }
|
||||
.kpi-card.total-projects { border-left-color: #28a745; }
|
||||
.kpi-card.active-tasks { border-left-color: #ffc107; }
|
||||
.kpi-card.overdue-tasks { border-left-color: #fd7e14; }
|
||||
.kpi-card.today-tasks { border-left-color: #6f42c1; }
|
||||
|
||||
.kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Ensure charts have proper dimensions */
|
||||
.chart-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
min-height: 300px; /* ✅ Critical: Give charts minimum height */
|
||||
position: relative; /* ✅ Required for Chart.js positioning */
|
||||
}
|
||||
|
||||
.chart-container canvas {
|
||||
max-height: 300px;
|
||||
width: 100% !important; /* ✅ Force full width */
|
||||
height: 250px !important; /* ✅ Force height */
|
||||
}
|
||||
|
||||
.chart-container h4 {
|
||||
margin: 0 0 20px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Responsive charts */
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
min-height: 250px;
|
||||
}
|
||||
.chart-container canvas {
|
||||
height: 200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table-container h4 {
|
||||
margin: 0 0 20px;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
padding: 12px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 5px 15px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Print Button */
|
||||
#btn_print_dashboard {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
#btn_print_dashboard:hover {
|
||||
background: #1e7e34;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.charts-row,
|
||||
.data-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kpi-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
/* Add these new styles */
|
||||
|
||||
/* Live indicator */
|
||||
.live-indicator {
|
||||
margin-top: 15px;
|
||||
padding: 8px 12px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.live-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #4caf50;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Clickable KPI cards */
|
||||
.kpi-card.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kpi-card.clickable:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.kpi-card.clickable .kpi-hint {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.kpi-card.clickable:hover .kpi-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Filter actions */
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.no-print,
|
||||
.filter-actions,
|
||||
.live-indicator,
|
||||
.pagination,
|
||||
.btn,
|
||||
.kpi-hint {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.kpi-card.clickable {
|
||||
cursor: default;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.o_project_dashboard {
|
||||
padding: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
box-shadow: none;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive improvements */
|
||||
@media (max-width: 768px) {
|
||||
.filter-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,573 @@
|
||||
/** @odoo-module **/
|
||||
import { Component, onMounted, onWillUnmount, useState, useRef } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class ProjectDashboard extends Component {
|
||||
static template = "project_dashboard_advanced.ProjectDashboard";
|
||||
|
||||
setup() {
|
||||
console.log("🚀 Dashboard Component Loaded with Live Updates");
|
||||
|
||||
this.state = useState({
|
||||
userName: 'User',
|
||||
userImage: '/web/static/img/user_avatar.png',
|
||||
greeting: 'Morning',
|
||||
filters: {
|
||||
manager_id: null,
|
||||
customer_id: null,
|
||||
project_id: null,
|
||||
date_range: 'lifetime',
|
||||
company_id: null
|
||||
},
|
||||
kpis: {},
|
||||
tasks: [],
|
||||
activities: [],
|
||||
tasksPage: 1,
|
||||
activitiesPage: 1,
|
||||
tasksLimit: 10,
|
||||
activitiesLimit: 10,
|
||||
tasksTotal: 0,
|
||||
activitiesTotal: 0,
|
||||
loaded: false,
|
||||
showCompanyFilter: false,
|
||||
lastUpdate: new Date().toLocaleTimeString(),
|
||||
refreshInterval: 15,
|
||||
autoRefresh: true,
|
||||
});
|
||||
|
||||
// Define Refs
|
||||
this.refsConfig = {
|
||||
filter_company: useRef("filter_company"),
|
||||
filter_manager: useRef("filter_manager"),
|
||||
filter_customer: useRef("filter_customer"),
|
||||
filter_project: useRef("filter_project"),
|
||||
filter_date: useRef("filter_date"),
|
||||
btn_refresh: useRef("btn_refresh"),
|
||||
btn_print_dashboard: useRef("btn_print_dashboard"),
|
||||
|
||||
kpi_my_tasks: useRef("kpi_my_tasks"),
|
||||
kpi_my_overdue: useRef("kpi_my_overdue"),
|
||||
kpi_total_projects: useRef("kpi_total_projects"),
|
||||
kpi_active_tasks: useRef("kpi_active_tasks"),
|
||||
kpi_overdue_tasks: useRef("kpi_overdue_tasks"),
|
||||
kpi_today_tasks: useRef("kpi_today_tasks"),
|
||||
|
||||
chart_tasks_by_stage: useRef("chart_tasks_by_stage"),
|
||||
chart_tasks_by_project: useRef("chart_tasks_by_project"),
|
||||
chart_timesheet_hours: useRef("chart_timesheet_hours"),
|
||||
chart_task_deadline: useRef("chart_task_deadline"),
|
||||
chart_priority_wise: useRef("chart_priority_wise"),
|
||||
|
||||
tasks_table_body: useRef("tasks_table_body"),
|
||||
btn_prev_tasks: useRef("btn_prev_tasks"),
|
||||
tasks_pagination: useRef("tasks_pagination"),
|
||||
btn_next_tasks: useRef("btn_next_tasks"),
|
||||
|
||||
activities_table_body: useRef("activities_table_body"),
|
||||
btn_prev_activities: useRef("btn_prev_activities"),
|
||||
activities_pagination: useRef("activities_pagination"),
|
||||
btn_next_activities: useRef("btn_next_activities"),
|
||||
};
|
||||
|
||||
this.charts = {};
|
||||
this.refreshInterval = null;
|
||||
|
||||
var self = this;
|
||||
onMounted(function () {
|
||||
console.log("📥 Fetching Dashboard Data...");
|
||||
self._getSession().then(function () {
|
||||
return self._loadCompanies();
|
||||
}).then(function () {
|
||||
return self._loadFilterOptions();
|
||||
}).then(function () {
|
||||
return self._loadAll();
|
||||
}).then(function () {
|
||||
self.state.loaded = true;
|
||||
self._startAutoRefresh();
|
||||
self._bindEvents();
|
||||
console.log("✅ Dashboard Load Complete");
|
||||
}).catch(function (err) {
|
||||
console.error("❌ Error loading dashboard:", err);
|
||||
});
|
||||
});
|
||||
|
||||
onWillUnmount(function () {
|
||||
self._stopAutoRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
_getGreeting() {
|
||||
var h = new Date().getHours();
|
||||
return h < 12 ? 'Morning' : h < 18 ? 'Afternoon' : 'Evening';
|
||||
}
|
||||
|
||||
_getSession() {
|
||||
var self = this;
|
||||
return fetch('/web/session/get_session_info', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'call', params: {} })
|
||||
}).then(function (resp) { return resp.json(); })
|
||||
.then(function (result) {
|
||||
if (result.result) {
|
||||
self.state.userName = result.result.name || 'User';
|
||||
self.state.userImage = result.result.uid
|
||||
? '/web/image?model=res.users&id=' + result.result.uid + '&field=image_128'
|
||||
: '/web/static/img/user_avatar.png';
|
||||
self.csrfToken = result.result.csrf_token;
|
||||
self.state.greeting = self._getGreeting();
|
||||
|
||||
// Check if user is admin/manager
|
||||
self.state.isAdmin = result.result.is_admin || result.result.uid === 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_call(route, params) {
|
||||
var self = this;
|
||||
return fetch(route, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': self.csrfToken || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: Math.random(),
|
||||
method: 'call',
|
||||
params: params || {}
|
||||
})
|
||||
}).then(function (resp) { return resp.json(); })
|
||||
.then(function (result) {
|
||||
if (result.error) throw new Error(result.error.data.message);
|
||||
return result.result;
|
||||
});
|
||||
}
|
||||
|
||||
_loadCompanies() {
|
||||
var self = this;
|
||||
return this._call('/project_dashboard_advanced/get_companies', {}).then(function (companies) {
|
||||
if (companies && companies.length > 1) {
|
||||
self.state.showCompanyFilter = true;
|
||||
self._populateSelect(self.refsConfig.filter_company.el, companies, 'All Companies');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_loadFilterOptions() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
self._call('/project_dashboard_advanced/get_managers', {}),
|
||||
self._call('/project_dashboard_advanced/get_customers', {})
|
||||
]).then(function (results) {
|
||||
self._populateSelect(self.refsConfig.filter_manager.el, results[0], 'All Managers');
|
||||
self._populateSelect(self.refsConfig.filter_customer.el, results[1], 'All Customer');
|
||||
return self._loadProjectOptions();
|
||||
});
|
||||
}
|
||||
|
||||
_loadProjectOptions() {
|
||||
var self = this;
|
||||
var params = {};
|
||||
if (self.state.filters.company_id) params.company_id = self.state.filters.company_id;
|
||||
if (self.state.filters.manager_id) params.manager_id = self.state.filters.manager_id;
|
||||
if (self.state.filters.customer_id) params.customer_id = self.state.filters.customer_id;
|
||||
return self._call('/project_dashboard_advanced/get_projects', params).then(function (projects) {
|
||||
self._populateSelect(self.refsConfig.filter_project.el, projects, 'All Projects');
|
||||
});
|
||||
}
|
||||
|
||||
_populateSelect(selectEl, items, defaultText) {
|
||||
if (!selectEl) return;
|
||||
selectEl.innerHTML = '<option value="">' + defaultText + '</option>';
|
||||
items.forEach(function (item) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = item.id;
|
||||
opt.textContent = item.name;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
_startAutoRefresh() {
|
||||
var self = this;
|
||||
this.refreshInterval = setInterval(function () {
|
||||
console.log("🔄 Auto-refreshing dashboard data...");
|
||||
self._loadAll().then(function () {
|
||||
self.state.lastUpdate = new Date().toLocaleTimeString();
|
||||
});
|
||||
}, 15000); // 15 seconds
|
||||
}
|
||||
|
||||
_stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
_loadAll() {
|
||||
var self = this;
|
||||
return Promise.all([
|
||||
self._loadKPIs(),
|
||||
self._loadCharts(),
|
||||
self._loadTasks(),
|
||||
self._loadActivities()
|
||||
]);
|
||||
}
|
||||
|
||||
_loadKPIs() {
|
||||
var self = this;
|
||||
var params = { filters: self.state.filters };
|
||||
if (self.state.filters.company_id) {
|
||||
params.company_id = self.state.filters.company_id;
|
||||
}
|
||||
|
||||
return self._call('/project_dashboard_advanced/get_kpis', params).then(function (kpis) {
|
||||
self.state.kpis = kpis;
|
||||
var kpiMap = {
|
||||
'my_tasks': kpis.my_tasks,
|
||||
'my_overdue': kpis.my_overdue_tasks,
|
||||
'total_projects': kpis.total_projects,
|
||||
'active_tasks': kpis.active_tasks,
|
||||
'overdue_tasks': kpis.overdue_tasks,
|
||||
'today_tasks': kpis.today_tasks
|
||||
};
|
||||
Object.keys(kpiMap).forEach(function (key) {
|
||||
var refName = 'kpi_' + key;
|
||||
var el = self.refsConfig[refName] ? self.refsConfig[refName].el : null;
|
||||
if (el) {
|
||||
var valueEl = el.querySelector('.kpi-value');
|
||||
if (valueEl) valueEl.textContent = kpiMap[key] || 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_loadCharts() {
|
||||
var self = this;
|
||||
var params = { filters: self.state.filters };
|
||||
if (self.state.filters.company_id) {
|
||||
params.company_id = self.state.filters.company_id;
|
||||
}
|
||||
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error("❌ Chart.js is NOT loaded!");
|
||||
return;
|
||||
}
|
||||
|
||||
return self._call('/project_dashboard_advanced/tasks_by_stage', params)
|
||||
.then(function (data) { self._renderChart('chart_tasks_by_stage', 'doughnut', data); })
|
||||
.then(function () { return self._call('/project_dashboard_advanced/tasks_by_project', params); })
|
||||
.then(function (data) { self._renderChart('chart_tasks_by_project', 'bar', data); })
|
||||
.then(function () { return self._call('/project_dashboard_advanced/timesheet_hours', params); })
|
||||
.then(function (data) { self._renderChart('chart_timesheet_hours', 'line', data); })
|
||||
.then(function () { return self._call('/project_dashboard_advanced/task_deadline', params); })
|
||||
.then(function (data) { self._renderChart('chart_task_deadline', 'pie', data); })
|
||||
.then(function () { return self._call('/project_dashboard_advanced/priority_wise', params); })
|
||||
.then(function (data) { self._renderChart('chart_priority_wise', 'bar', data); });
|
||||
}
|
||||
|
||||
_renderChart(refName, type, data) {
|
||||
var canvasEl = this.refsConfig[refName] ? this.refsConfig[refName].el : null;
|
||||
if (!canvasEl) return;
|
||||
|
||||
if (this.charts[refName]) this.charts[refName].destroy();
|
||||
|
||||
var labels = [];
|
||||
var values = [];
|
||||
var colors = ['#007bff','#28a745','#ffc107','#dc3545','#6f42c1','#17a2b8','#6610f2','#fd7e14'];
|
||||
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
data.forEach(function (item, index) {
|
||||
labels.push(item.name || 'Category ' + (index + 1));
|
||||
values.push(item.value || 0);
|
||||
});
|
||||
} else {
|
||||
labels.push('No Data');
|
||||
values.push(0);
|
||||
}
|
||||
|
||||
var chartDataObj = {
|
||||
labels: labels,
|
||||
datasets: [{ data: values, backgroundColor: colors, borderWidth: 1 }]
|
||||
};
|
||||
|
||||
var chartConfig = {
|
||||
type: type,
|
||||
data: chartDataObj,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { position: 'bottom' } }
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.charts[refName] = new Chart(canvasEl, chartConfig);
|
||||
} catch (e) {
|
||||
console.error("Chart Error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
_loadTasks() {
|
||||
var self = this;
|
||||
var offset = (self.state.tasksPage - 1) * self.state.tasksLimit;
|
||||
var params = {
|
||||
filters: self.state.filters,
|
||||
limit: self.state.tasksLimit,
|
||||
offset: offset
|
||||
};
|
||||
if (self.state.filters.company_id) {
|
||||
params.company_id = self.state.filters.company_id;
|
||||
}
|
||||
|
||||
return self._call('/project_dashboard_advanced/get_all_tasks', params).then(function (result) {
|
||||
self.state.tasks = result.tasks || [];
|
||||
self.state.tasksTotal = result.total || 0;
|
||||
self._renderTasksTable();
|
||||
self._updateTasksPagination();
|
||||
});
|
||||
}
|
||||
|
||||
_renderTasksTable() {
|
||||
var tbodyEl = this.refsConfig.tasks_table_body ? this.refsConfig.tasks_table_body.el : null;
|
||||
if (!tbodyEl) return;
|
||||
|
||||
if (this.state.tasks.length === 0) {
|
||||
tbodyEl.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No tasks found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var html = '';
|
||||
this.state.tasks.forEach(function (t) {
|
||||
var badgeClass = 'bg-success';
|
||||
if (t.status === 'overdue') badgeClass = 'bg-danger';
|
||||
else if (t.status === 'today') badgeClass = 'bg-warning';
|
||||
|
||||
html += '<tr>' +
|
||||
'<td>' + (t.name || '') + '</td>' +
|
||||
'<td>' + (t.project || '-') + '</td>' +
|
||||
'<td><span class="badge ' + badgeClass + '">' + (t.date_deadline || '-') + '</span></td>' +
|
||||
'<td><span class="badge bg-secondary">' + (t.priority_label || '-') + '</span></td>' +
|
||||
'<td><button class="btn btn-sm btn-primary btn-view-task" data-task-id="' + t.id + '">View</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
tbodyEl.innerHTML = html;
|
||||
|
||||
tbodyEl.querySelectorAll('.btn-view-task').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var taskId = this.getAttribute('data-task-id');
|
||||
self._openTask(taskId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_updateTasksPagination() {
|
||||
var maxPage = Math.ceil(this.state.tasksTotal / this.state.tasksLimit) || 1;
|
||||
var pagEl = this.refsConfig.tasks_pagination ? this.refsConfig.tasks_pagination.el : null;
|
||||
if (pagEl) pagEl.textContent = 'Page ' + this.state.tasksPage + ' of ' + maxPage;
|
||||
|
||||
var prevBtn = this.refsConfig.btn_prev_tasks ? this.refsConfig.btn_prev_tasks.el : null;
|
||||
var nextBtn = this.refsConfig.btn_next_tasks ? this.refsConfig.btn_next_tasks.el : null;
|
||||
|
||||
if (prevBtn) prevBtn.disabled = this.state.tasksPage <= 1;
|
||||
if (nextBtn) nextBtn.disabled = this.state.tasksPage >= maxPage;
|
||||
}
|
||||
|
||||
_loadActivities() {
|
||||
var self = this;
|
||||
var offset = (self.state.activitiesPage - 1) * self.state.activitiesLimit;
|
||||
return self._call('/project_dashboard_advanced/get_activities', {
|
||||
filters: self.state.filters,
|
||||
limit: self.state.activitiesLimit,
|
||||
offset: offset
|
||||
}).then(function (result) {
|
||||
self.state.activities = result.activities || [];
|
||||
self.state.activitiesTotal = result.total || 0;
|
||||
self._renderActivitiesTable();
|
||||
self._updateActivitiesPagination();
|
||||
});
|
||||
}
|
||||
|
||||
_renderActivitiesTable() {
|
||||
var tbodyEl = this.refsConfig.activities_table_body ? this.refsConfig.activities_table_body.el : null;
|
||||
if (!tbodyEl) return;
|
||||
|
||||
if (this.state.activities.length === 0) {
|
||||
tbodyEl.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No activities found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var html = '';
|
||||
this.state.activities.forEach(function (a) {
|
||||
html += '<tr>' +
|
||||
'<td>' + (a.task || '-') + '</td>' +
|
||||
'<td>' + (a.activity || '-') + '</td>' +
|
||||
'<td>' + (a.summary || '-') + '</td>' +
|
||||
'<td>' + (a.date || '-') + '</td>' +
|
||||
'<td><button class="btn btn-sm btn-primary btn-view-activity" data-activity-id="' + a.id + '">View</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
tbodyEl.innerHTML = html;
|
||||
|
||||
tbodyEl.querySelectorAll('.btn-view-activity').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var activityId = this.getAttribute('data-activity-id');
|
||||
self._openActivity(activityId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_updateActivitiesPagination() {
|
||||
var maxPage = Math.ceil(this.state.activitiesTotal / this.state.activitiesLimit) || 1;
|
||||
var pagEl = this.refsConfig.activities_pagination ? this.refsConfig.activities_pagination.el : null;
|
||||
if (pagEl) pagEl.textContent = 'Page ' + this.state.activitiesPage + ' of ' + maxPage;
|
||||
|
||||
var prevBtn = this.refsConfig.btn_prev_activities ? this.refsConfig.btn_prev_activities.el : null;
|
||||
var nextBtn = this.refsConfig.btn_next_activities ? this.refsConfig.btn_next_activities.el : null;
|
||||
|
||||
if (prevBtn) prevBtn.disabled = this.state.activitiesPage <= 1;
|
||||
if (nextBtn) nextBtn.disabled = this.state.activitiesPage >= maxPage;
|
||||
}
|
||||
|
||||
_openTask(taskId) {
|
||||
var id = parseInt(taskId);
|
||||
if (isNaN(id)) return;
|
||||
window.location.href = '/web#model=project.task&id=' + id + '&view_type=form';
|
||||
}
|
||||
|
||||
_openActivity(activityId) {
|
||||
var id = parseInt(activityId);
|
||||
if (isNaN(id)) return;
|
||||
window.location.href = '/web#model=mail.activity&id=' + id + '&view_type=form';
|
||||
}
|
||||
|
||||
_handleKPIClick(filterType) {
|
||||
console.log("📊 KPI clicked:", filterType);
|
||||
// Apply filter based on KPI type
|
||||
var filters = {};
|
||||
|
||||
switch(filterType) {
|
||||
case 'my_tasks':
|
||||
filters.user_id = this.state.userName; // This would need backend support
|
||||
break;
|
||||
case 'my_overdue':
|
||||
filters.status = 'overdue';
|
||||
break;
|
||||
case 'total_projects':
|
||||
// Open projects view
|
||||
window.location.href = '/web#model=project.project&view_type=list';
|
||||
return;
|
||||
case 'active_tasks':
|
||||
filters.stage = 'active';
|
||||
break;
|
||||
case 'overdue_tasks':
|
||||
filters.status = 'overdue';
|
||||
break;
|
||||
case 'today_tasks':
|
||||
filters.date_deadline = 'today';
|
||||
break;
|
||||
}
|
||||
|
||||
// Reload tasks with new filter
|
||||
this.state.filters = Object.assign({}, this.state.filters, filters);
|
||||
this._loadTasks();
|
||||
}
|
||||
|
||||
_printReport() {
|
||||
console.log("🖨️ Printing dashboard report...");
|
||||
window.print();
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
var self = this;
|
||||
|
||||
// Company filter
|
||||
if (this.refsConfig.filter_company.el) {
|
||||
this.refsConfig.filter_company.el.addEventListener('change', function (e) {
|
||||
var value = e.target.value;
|
||||
self.state.filters.company_id = value ? parseInt(value) : null;
|
||||
self._loadProjectOptions().then(function () { return self._loadAll(); });
|
||||
});
|
||||
}
|
||||
|
||||
// Other filters
|
||||
var filterEls = [
|
||||
{ ref: this.refsConfig.filter_manager, name: 'manager' },
|
||||
{ ref: this.refsConfig.filter_customer, name: 'customer' },
|
||||
{ ref: this.refsConfig.filter_project, name: 'project' },
|
||||
{ ref: this.refsConfig.filter_date, name: 'date' }
|
||||
];
|
||||
|
||||
filterEls.forEach(function (item) {
|
||||
if (item.ref && item.ref.el) {
|
||||
item.ref.el.addEventListener('change', function (e) {
|
||||
var value = e.target.value;
|
||||
self.state.filters[item.name + '_id'] =
|
||||
item.name === 'date' ? value : (value ? parseInt(value) : null);
|
||||
|
||||
if (item.name === 'manager' || item.name === 'customer') {
|
||||
self._loadProjectOptions().then(function () { return self._loadAll(); });
|
||||
} else {
|
||||
self._loadAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// KPI Cards Click Handlers
|
||||
var kpiCards = [
|
||||
{ ref: this.refsConfig.kpi_my_tasks, filter: 'my_tasks' },
|
||||
{ ref: this.refsConfig.kpi_my_overdue, filter: 'my_overdue' },
|
||||
{ ref: this.refsConfig.kpi_total_projects, filter: 'total_projects' },
|
||||
{ ref: this.refsConfig.kpi_active_tasks, filter: 'active_tasks' },
|
||||
{ ref: this.refsConfig.kpi_overdue_tasks, filter: 'overdue_tasks' },
|
||||
{ ref: this.refsConfig.kpi_today_tasks, filter: 'today_tasks' },
|
||||
];
|
||||
|
||||
kpiCards.forEach(function(kpi) {
|
||||
if (kpi.ref && kpi.ref.el) {
|
||||
kpi.ref.el.style.cursor = 'pointer';
|
||||
kpi.ref.el.addEventListener('click', function() {
|
||||
self._handleKPIClick(kpi.filter);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh button
|
||||
if (this.refsConfig.btn_refresh.el) {
|
||||
this.refsConfig.btn_refresh.el.addEventListener('click', function () {
|
||||
console.log("🔄 Manual refresh triggered");
|
||||
self._loadAll().then(function () {
|
||||
self.state.lastUpdate = new Date().toLocaleTimeString();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Print button
|
||||
if (this.refsConfig.btn_print_dashboard.el) {
|
||||
this.refsConfig.btn_print_dashboard.el.addEventListener('click', function () {
|
||||
self._printReport();
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination buttons
|
||||
var btns = [
|
||||
{ el: this.refsConfig.btn_prev_tasks.el, action: () => { if (self.state.tasksPage > 1) { self.state.tasksPage--; self._loadTasks(); } } },
|
||||
{ el: this.refsConfig.btn_next_tasks.el, action: () => { var max = Math.ceil(self.state.tasksTotal / self.state.tasksLimit); if (self.state.tasksPage < max) { self.state.tasksPage++; self._loadTasks(); } } },
|
||||
{ el: this.refsConfig.btn_prev_activities.el, action: () => { if (self.state.activitiesPage > 1) { self.state.activitiesPage--; self._loadActivities(); } } },
|
||||
{ el: this.refsConfig.btn_next_activities.el, action: () => { var max = Math.ceil(self.state.activitiesTotal / self.state.activitiesLimit); if (self.state.activitiesPage < max) { self.state.activitiesPage++; self._loadActivities(); } } },
|
||||
];
|
||||
|
||||
btns.forEach(function(btn) {
|
||||
if (btn.el) btn.el.addEventListener('click', btn.action);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category('actions').add('project_dashboard_tag', ProjectDashboard);
|
||||
@@ -0,0 +1,167 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="project_dashboard_advanced.ProjectDashboard" owl="1">
|
||||
<div class="o_project_dashboard">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="dashboard-header">
|
||||
<div class="greeting-section">
|
||||
<div class="user-avatar">
|
||||
<img t-att-src="state.userImage" alt="User" class="img-circle"/>
|
||||
</div>
|
||||
<div class="greeting-text">
|
||||
<!-- <h3>Good <t t-esc="state.greeting"/>, <t t-esc="state.userName"/></h3>-->
|
||||
<h3><t t-esc="state.userName"/></h3>
|
||||
<p>My Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters-section">
|
||||
<!-- Company Filter -->
|
||||
<div class="filter-group" t-if="state.showCompanyFilter">
|
||||
<label>Company</label>
|
||||
<select t-ref="filter_company" class="form-control">
|
||||
<option value="">All Companies</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Managers</label>
|
||||
<select t-ref="filter_manager" class="form-control">
|
||||
<option value="">All Managers</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Customers</label>
|
||||
<select t-ref="filter_customer" class="form-control">
|
||||
<option value="">All Customer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Projects</label>
|
||||
<select t-ref="filter_project" class="form-control">
|
||||
<option value="">All Projects</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Date</label>
|
||||
<select t-ref="filter_date" class="form-control">
|
||||
<option value="lifetime">Lifetime</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">This Week</option>
|
||||
<option value="month">This Month</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<button t-ref="btn_refresh" class="btn btn-secondary" title="Refresh (15s)">
|
||||
<i class="fa fa-refresh"/> Refresh
|
||||
</button>
|
||||
<button t-ref="btn_print_dashboard" class="btn btn-primary">
|
||||
<i class="fa fa-print"/> Print Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live indicator -->
|
||||
<div class="live-indicator">
|
||||
<span class="live-dot"/>
|
||||
<span>Live Update: <t t-esc="state.lastUpdate"/> (<t t-esc="state.refreshInterval"/>s)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI CARDS - Clickable -->
|
||||
<div class="kpi-cards">
|
||||
<div class="kpi-card my-tasks clickable" t-ref="kpi_my_tasks" data-filter="my_tasks">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">My Tasks</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
<div class="kpi-card my-overdue clickable" t-ref="kpi_my_overdue" data-filter="my_overdue">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">My Overdue</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
<div class="kpi-card total-projects clickable" t-ref="kpi_total_projects" data-filter="total_projects">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">Total Projects</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
<div class="kpi-card active-tasks clickable" t-ref="kpi_active_tasks" data-filter="active_tasks">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">Active Tasks</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
<div class="kpi-card overdue-tasks clickable" t-ref="kpi_overdue_tasks" data-filter="overdue_tasks">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">Overdue Tasks</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
<div class="kpi-card today-tasks clickable" t-ref="kpi_today_tasks" data-filter="today_tasks">
|
||||
<div class="kpi-value">0</div>
|
||||
<div class="kpi-label">Today Tasks</div>
|
||||
<div class="kpi-hint">Click to view</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CHARTS -->
|
||||
<div class="charts-row">
|
||||
<div class="chart-container">
|
||||
<h4>Task By Stages</h4>
|
||||
<canvas t-ref="chart_tasks_by_stage"/>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h4>Task By Project</h4>
|
||||
<canvas t-ref="chart_tasks_by_project"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TASKS TABLE & DEADLINE CHART -->
|
||||
<div class="data-row">
|
||||
<div class="table-container">
|
||||
<h4>All Tasks</h4>
|
||||
<table class="table table-striped">
|
||||
<thead><tr><th>Name</th><th>Project</th><th>Deadline</th><th>Priority</th><th>Action</th></tr></thead>
|
||||
<tbody t-ref="tasks_table_body"/>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<button t-ref="btn_prev_tasks" class="btn btn-sm">Previous</button>
|
||||
<span t-ref="tasks_pagination">Page 1 of 1</span>
|
||||
<button t-ref="btn_next_tasks" class="btn btn-sm">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h4>Task Deadline</h4>
|
||||
<canvas t-ref="chart_task_deadline"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ACTIVITIES & PRIORITY CHART -->
|
||||
<div class="data-row">
|
||||
<div class="table-container">
|
||||
<h4>Activities</h4>
|
||||
<table class="table table-striped">
|
||||
<thead><tr><th>Task</th><th>Activity</th><th>Summary</th><th>Date</th><th>Action</th></tr></thead>
|
||||
<tbody t-ref="activities_table_body"/>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<button t-ref="btn_prev_activities" class="btn btn-sm">Previous</button>
|
||||
<span t-ref="activities_pagination">Page 1 of 1</span>
|
||||
<button t-ref="btn_next_activities" class="btn btn-sm">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h4>Priority Wise</h4>
|
||||
<canvas t-ref="chart_priority_wise"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container full-width">
|
||||
<h4>Timesheet Hours</h4>
|
||||
<canvas t-ref="chart_timesheet_hours"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user