first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
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>