Spaces:
Running
Running
Xây dựng một hệ thống đầy đủ và hoàn chỉnh quản lý KPI, nhân viên có thể cập nhật KPI thực hiện hàng ngày, tổ trưởng cũng có thể cập nhật cho nhân viên trong tổ thống kê và xem trong tổ, admin quản lý tất cả bao gồm thống kê, xuất excel.
Browse files- components/kpi-form.js +70 -0
- components/kpi-team-view.js +63 -0
- components/navbar.js +4 -2
- index.html +2 -5
- script.js +11 -53
components/kpi-form.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class KpiForm extends HTMLElement {
|
| 2 |
+
connectedCallback() {
|
| 3 |
+
this.attachShadow({ mode: 'open' });
|
| 4 |
+
this.shadowRoot.innerHTML = `
|
| 5 |
+
<style>
|
| 6 |
+
.kpi-form {
|
| 7 |
+
@apply bg-white rounded-lg shadow-md p-6 mb-6;
|
| 8 |
+
}
|
| 9 |
+
.form-title {
|
| 10 |
+
@apply text-xl font-semibold text-gray-800 mb-4;
|
| 11 |
+
}
|
| 12 |
+
.form-group {
|
| 13 |
+
@apply mb-4;
|
| 14 |
+
}
|
| 15 |
+
label {
|
| 16 |
+
@apply block text-sm font-medium text-gray-700 mb-1;
|
| 17 |
+
}
|
| 18 |
+
input, select, textarea {
|
| 19 |
+
@apply w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500;
|
| 20 |
+
}
|
| 21 |
+
.submit-btn {
|
| 22 |
+
@apply w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200;
|
| 23 |
+
}
|
| 24 |
+
</style>
|
| 25 |
+
<div class="kpi-form">
|
| 26 |
+
<h3 class="form-title">Update Daily KPI</h3>
|
| 27 |
+
<form id="kpiForm">
|
| 28 |
+
<div class="form-group">
|
| 29 |
+
<label for="kpiDate">Date</label>
|
| 30 |
+
<input type="date" id="kpiDate" required>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="form-group">
|
| 33 |
+
<label for="kpiMetric">KPI Metric</label>
|
| 34 |
+
<select id="kpiMetric" required>
|
| 35 |
+
<option value="">Select Metric</option>
|
| 36 |
+
<option value="productivity">Productivity</option>
|
| 37 |
+
<option value="quality">Quality</option>
|
| 38 |
+
<option value="efficiency">Efficiency</option>
|
| 39 |
+
<option value="attendance">Attendance</option>
|
| 40 |
+
</select>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="form-group">
|
| 43 |
+
<label for="kpiValue">Value</label>
|
| 44 |
+
<input type="number" id="kpiValue" min="0" max="100" required>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="form-group">
|
| 47 |
+
<label for="kpiNotes">Notes</label>
|
| 48 |
+
<textarea id="kpiNotes" rows="3"></textarea>
|
| 49 |
+
</div>
|
| 50 |
+
<button type="submit" class="submit-btn">Submit KPI</button>
|
| 51 |
+
</form>
|
| 52 |
+
</div>
|
| 53 |
+
`;
|
| 54 |
+
|
| 55 |
+
this.shadowRoot.getElementById('kpiForm').addEventListener('submit', (e) => {
|
| 56 |
+
e.preventDefault();
|
| 57 |
+
const newKpi = {
|
| 58 |
+
date: this.shadowRoot.getElementById('kpiDate').value,
|
| 59 |
+
metric: this.shadowRoot.getElementById('kpiMetric').value,
|
| 60 |
+
value: parseFloat(this.shadowRoot.getElementById('kpiValue').value),
|
| 61 |
+
notes: this.shadowRoot.getElementById('kpiNotes').value,
|
| 62 |
+
timestamp: new Date().toISOString()
|
| 63 |
+
};
|
| 64 |
+
this.dispatchEvent(new CustomEvent('kpi-submitted', { detail: newKpi }));
|
| 65 |
+
this.shadowRoot.getElementById('kpiForm').reset();
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
customElements.define('kpi-form', KpiForm);
|
components/kpi-team-view.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```javascript
|
| 2 |
+
class KpiTeamView extends HTMLElement {
|
| 3 |
+
connectedCallback() {
|
| 4 |
+
this.attachShadow({ mode: 'open' });
|
| 5 |
+
this.shadowRoot.innerHTML = `
|
| 6 |
+
<style>
|
| 7 |
+
.team-view {
|
| 8 |
+
@apply bg-white rounded-lg shadow-md p-6;
|
| 9 |
+
}
|
| 10 |
+
.view-header {
|
| 11 |
+
@apply flex justify-between items-center mb-6;
|
| 12 |
+
}
|
| 13 |
+
.view-title {
|
| 14 |
+
@apply text-xl font-semibold text-gray-800;
|
| 15 |
+
}
|
| 16 |
+
.export-btn {
|
| 17 |
+
@apply bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium;
|
| 18 |
+
}
|
| 19 |
+
.team-table {
|
| 20 |
+
@apply w-full border-collapse;
|
| 21 |
+
}
|
| 22 |
+
.team-table th {
|
| 23 |
+
@apply text-left py-3 px-4 bg-gray-50 text-gray-600 text-sm font-medium;
|
| 24 |
+
}
|
| 25 |
+
.team-table td {
|
| 26 |
+
@apply py-3 px-4 border-b border-gray-200;
|
| 27 |
+
}
|
| 28 |
+
.progress-bar {
|
| 29 |
+
@apply h-2 bg-gray-200 rounded-full overflow-hidden;
|
| 30 |
+
}
|
| 31 |
+
.progress-value {
|
| 32 |
+
@apply h-full;
|
| 33 |
+
}
|
| 34 |
+
.good {
|
| 35 |
+
@apply bg-green-500;
|
| 36 |
+
}
|
| 37 |
+
.average {
|
| 38 |
+
@apply bg-yellow-500;
|
| 39 |
+
}
|
| 40 |
+
.poor {
|
| 41 |
+
@apply bg-red-500;
|
| 42 |
+
}
|
| 43 |
+
</style>
|
| 44 |
+
<div class="team-view">
|
| 45 |
+
<div class="view-header">
|
| 46 |
+
<h3 class="view-title">Team KPI Overview</h3>
|
| 47 |
+
<button class="export-btn" id="exportExcel">
|
| 48 |
+
<i data-feather="download"></i> Export to Excel
|
| 49 |
+
</button>
|
| 50 |
+
</div>
|
| 51 |
+
<table class="team-table">
|
| 52 |
+
<thead>
|
| 53 |
+
<tr>
|
| 54 |
+
<th>Employee</th>
|
| 55 |
+
<th>Productivity</th>
|
| 56 |
+
<th>Quality</th>
|
| 57 |
+
<th>Efficiency</th>
|
| 58 |
+
<th>Attendance</th>
|
| 59 |
+
<th>Overall</th>
|
| 60 |
+
</tr>
|
| 61 |
+
</thead>
|
| 62 |
+
<tbody id="teamData">
|
| 63 |
+
<!-- Team data will be populated here -->
|
components/navbar.js
CHANGED
|
@@ -50,8 +50,10 @@ class CustomNavbar extends HTMLElement {
|
|
| 50 |
<i data-feather="calendar"></i>
|
| 51 |
<span>Calendar</span>
|
| 52 |
</a>
|
| 53 |
-
<user-profile>
|
| 54 |
-
</div>
|
|
|
|
|
|
|
| 55 |
</nav>
|
| 56 |
`;
|
| 57 |
}
|
|
|
|
| 50 |
<i data-feather="calendar"></i>
|
| 51 |
<span>Calendar</span>
|
| 52 |
</a>
|
| 53 |
+
<div class="user-profile">
|
| 54 |
+
<div class="avatar">JD</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
</nav>
|
| 58 |
`;
|
| 59 |
}
|
index.html
CHANGED
|
@@ -22,8 +22,7 @@
|
|
| 22 |
<main class="flex-1 p-8">
|
| 23 |
<div class="mb-8">
|
| 24 |
<h1 class="text-3xl font-bold text-gray-800 mb-4">Dashboard</h1>
|
| 25 |
-
<
|
| 26 |
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 27 |
<div class="bg-white rounded-xl shadow-md p-6">
|
| 28 |
<h2 class="text-xl font-semibold text-gray-700 mb-4 flex items-center">
|
| 29 |
<i data-feather="check-circle" class="mr-2 text-green-500"></i>
|
|
@@ -72,9 +71,7 @@
|
|
| 72 |
</div>
|
| 73 |
</main>
|
| 74 |
</div>
|
| 75 |
-
|
| 76 |
-
<script src="components/task-filter.js"></script>
|
| 77 |
-
<script src="components/user-profile.js"></script>
|
| 78 |
<script src="script.js"></script>
|
| 79 |
<script>
|
| 80 |
feather.replace();
|
|
|
|
| 22 |
<main class="flex-1 p-8">
|
| 23 |
<div class="mb-8">
|
| 24 |
<h1 class="text-3xl font-bold text-gray-800 mb-4">Dashboard</h1>
|
| 25 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
| 26 |
<div class="bg-white rounded-xl shadow-md p-6">
|
| 27 |
<h2 class="text-xl font-semibold text-gray-700 mb-4 flex items-center">
|
| 28 |
<i data-feather="check-circle" class="mr-2 text-green-500"></i>
|
|
|
|
| 71 |
</div>
|
| 72 |
</main>
|
| 73 |
</div>
|
| 74 |
+
|
|
|
|
|
|
|
| 75 |
<script src="script.js"></script>
|
| 76 |
<script>
|
| 77 |
feather.replace();
|
script.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
| 1 |
|
| 2 |
document.addEventListener('DOMContentLoaded', function() {
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
if (!currentUser) {
|
| 6 |
-
document.body.innerHTML = '<auth-form></auth-form>';
|
| 7 |
-
document.querySelector('auth-form').addEventListener('auth-success', () => {
|
| 8 |
-
window.location.reload();
|
| 9 |
-
});
|
| 10 |
-
return;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
// Initialize tasks for current user
|
| 14 |
-
const userTasksKey = `tasks_${currentUser.email}`;
|
| 15 |
-
let tasks = JSON.parse(localStorage.getItem(userTasksKey)) || [
|
| 16 |
-
{ id: 1, title: 'Complete project proposal', dueDate: new Date().toISOString().split('T')[0], priority: 'high', completed: false },
|
| 17 |
{ id: 2, title: 'Review team documents', dueDate: new Date(Date.now() + 86400000).toISOString().split('T')[0], priority: 'medium', completed: false },
|
| 18 |
{ id: 3, title: 'Schedule meeting with client', dueDate: new Date(Date.now() - 86400000).toISOString().split('T')[0], priority: 'low', completed: true }
|
| 19 |
];
|
|
@@ -30,23 +18,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 30 |
...e.detail
|
| 31 |
};
|
| 32 |
tasks.push(newTask);
|
| 33 |
-
localStorage.setItem(
|
| 34 |
-
renderTasks();
|
| 35 |
updateKpiReport();
|
| 36 |
});
|
| 37 |
-
// Handle task filtering
|
| 38 |
-
document.addEventListener('filter-changed', (e) => {
|
| 39 |
-
renderTasks(e.detail);
|
| 40 |
-
});
|
| 41 |
|
| 42 |
// Complete task
|
| 43 |
-
document.addEventListener('task-completed', (e) => {
|
| 44 |
const taskId = parseInt(e.detail.taskId);
|
| 45 |
tasks = tasks.map(task =>
|
| 46 |
task.id === taskId ? {...task, completed: !task.completed} : task
|
| 47 |
);
|
| 48 |
-
localStorage.setItem(
|
| 49 |
-
renderTasks();
|
| 50 |
updateKpiReport();
|
| 51 |
});
|
| 52 |
const documents = [
|
|
@@ -54,37 +38,11 @@ const documents = [
|
|
| 54 |
{ id: 2, title: 'Meeting Notes', type: 'doc', lastModified: '2023-06-03', size: '1.1 MB' },
|
| 55 |
{ id: 3, title: 'Budget Plan', type: 'xls', lastModified: '2023-05-28', size: '3.7 MB' }
|
| 56 |
];
|
| 57 |
-
// Filter and render tasks
|
| 58 |
-
function filterTasks(tasks, filters = {}) {
|
| 59 |
-
return tasks.filter(task => {
|
| 60 |
-
let matches = true;
|
| 61 |
-
|
| 62 |
-
if (filters.status === 'completed') {
|
| 63 |
-
matches = matches && task.completed;
|
| 64 |
-
} else if (filters.status === 'pending') {
|
| 65 |
-
matches = matches && !task.completed && new Date(task.dueDate) >= new Date();
|
| 66 |
-
} else if (filters.status === 'overdue') {
|
| 67 |
-
matches = matches && !task.completed && new Date(task.dueDate) < new Date();
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (filters.priority && filters.priority !== 'all') {
|
| 71 |
-
matches = matches && task.priority === filters.priority;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
if (filters.date) {
|
| 75 |
-
matches = matches && task.dueDate === filters.date;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
return matches;
|
| 79 |
-
});
|
| 80 |
-
}
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
filteredTasks.forEach(task => {
|
| 87 |
-
const taskCard = document.createElement('custom-task-card');
|
| 88 |
taskCard.setAttribute('task-id', task.id);
|
| 89 |
taskCard.setAttribute('task-title', task.title);
|
| 90 |
taskCard.setAttribute('due-date', task.dueDate);
|
|
|
|
| 1 |
|
| 2 |
document.addEventListener('DOMContentLoaded', function() {
|
| 3 |
+
let tasks = JSON.parse(localStorage.getItem('tasks')) || [
|
| 4 |
+
{ id: 1, title: 'Complete project proposal', dueDate: new Date().toISOString().split('T')[0], priority: 'high', completed: false },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
{ id: 2, title: 'Review team documents', dueDate: new Date(Date.now() + 86400000).toISOString().split('T')[0], priority: 'medium', completed: false },
|
| 6 |
{ id: 3, title: 'Schedule meeting with client', dueDate: new Date(Date.now() - 86400000).toISOString().split('T')[0], priority: 'low', completed: true }
|
| 7 |
];
|
|
|
|
| 18 |
...e.detail
|
| 19 |
};
|
| 20 |
tasks.push(newTask);
|
| 21 |
+
localStorage.setItem('tasks', JSON.stringify(tasks));
|
| 22 |
+
renderTasks();
|
| 23 |
updateKpiReport();
|
| 24 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
// Complete task
|
| 27 |
+
document.addEventListener('task-completed', (e) => {
|
| 28 |
const taskId = parseInt(e.detail.taskId);
|
| 29 |
tasks = tasks.map(task =>
|
| 30 |
task.id === taskId ? {...task, completed: !task.completed} : task
|
| 31 |
);
|
| 32 |
+
localStorage.setItem('tasks', JSON.stringify(tasks));
|
| 33 |
+
renderTasks();
|
| 34 |
updateKpiReport();
|
| 35 |
});
|
| 36 |
const documents = [
|
|
|
|
| 38 |
{ id: 2, title: 'Meeting Notes', type: 'doc', lastModified: '2023-06-03', size: '1.1 MB' },
|
| 39 |
{ id: 3, title: 'Budget Plan', type: 'xls', lastModified: '2023-05-28', size: '3.7 MB' }
|
| 40 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
// Render tasks
|
| 43 |
+
const tasksContainer = document.getElementById('tasks-container');
|
| 44 |
+
tasks.forEach(task => {
|
| 45 |
+
const taskCard = document.createElement('custom-task-card');
|
|
|
|
|
|
|
| 46 |
taskCard.setAttribute('task-id', task.id);
|
| 47 |
taskCard.setAttribute('task-title', task.title);
|
| 48 |
taskCard.setAttribute('due-date', task.dueDate);
|