Xây dựng một hệ thống đầy đủ, hoàn chỉnh việc quản lý KPI của đơn vị, người dùng đăng nhập và cập nhật KPI thực hiện hành ngày, tổ trưỡng có thể cập nhật KPI của nhân viên, quản lý KPI trong tổ, Admin quản lý tất cả, cập nhật danh sách KPI, thống kê, xuất dữ liệu ra excel
Browse files- README.md +8 -5
- components/auth-form.js +165 -0
- index.html +18 -19
- script.js +54 -0
- style.css +20 -18
README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: KPI Tracker Pro - Performance Pulse 💼
|
| 3 |
+
colorFrom: gray
|
| 4 |
+
colorTo: red
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
components/auth-form.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class AuthForm extends HTMLElement {
|
| 2 |
+
connectedCallback() {
|
| 3 |
+
this.attachShadow({ mode: 'open' });
|
| 4 |
+
this.shadowRoot.innerHTML = `
|
| 5 |
+
<style>
|
| 6 |
+
.auth-container {
|
| 7 |
+
background: white;
|
| 8 |
+
border-radius: 0.75rem;
|
| 9 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 10 |
+
width: 100%;
|
| 11 |
+
max-width: 28rem;
|
| 12 |
+
padding: 2rem;
|
| 13 |
+
}
|
| 14 |
+
.auth-title {
|
| 15 |
+
font-size: 1.5rem;
|
| 16 |
+
font-weight: 600;
|
| 17 |
+
margin-bottom: 1.5rem;
|
| 18 |
+
color: #1a365d;
|
| 19 |
+
text-align: center;
|
| 20 |
+
}
|
| 21 |
+
.input-group {
|
| 22 |
+
margin-bottom: 1.25rem;
|
| 23 |
+
}
|
| 24 |
+
.input-label {
|
| 25 |
+
display: block;
|
| 26 |
+
margin-bottom: 0.5rem;
|
| 27 |
+
font-size: 0.875rem;
|
| 28 |
+
font-weight: 500;
|
| 29 |
+
color: #4a5568;
|
| 30 |
+
}
|
| 31 |
+
.input-field {
|
| 32 |
+
width: 100%;
|
| 33 |
+
padding: 0.75rem;
|
| 34 |
+
border: 1px solid #e2e8f0;
|
| 35 |
+
border-radius: 0.375rem;
|
| 36 |
+
font-size: 0.875rem;
|
| 37 |
+
transition: border-color 0.2s;
|
| 38 |
+
}
|
| 39 |
+
.input-field:focus {
|
| 40 |
+
outline: none;
|
| 41 |
+
border-color: #4299e1;
|
| 42 |
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
|
| 43 |
+
}
|
| 44 |
+
.submit-btn {
|
| 45 |
+
width: 100%;
|
| 46 |
+
padding: 0.75rem;
|
| 47 |
+
background-color: #4299e1;
|
| 48 |
+
color: white;
|
| 49 |
+
font-weight: 500;
|
| 50 |
+
border: none;
|
| 51 |
+
border-radius: 0.375rem;
|
| 52 |
+
cursor: pointer;
|
| 53 |
+
transition: background-color 0.2s;
|
| 54 |
+
}
|
| 55 |
+
.submit-btn:hover {
|
| 56 |
+
background-color: #3182ce;
|
| 57 |
+
}
|
| 58 |
+
.toggle-auth {
|
| 59 |
+
text-align: center;
|
| 60 |
+
margin-top: 1rem;
|
| 61 |
+
font-size: 0.875rem;
|
| 62 |
+
color: #4a5568;
|
| 63 |
+
}
|
| 64 |
+
.toggle-link {
|
| 65 |
+
color: #4299e1;
|
| 66 |
+
font-weight: 500;
|
| 67 |
+
cursor: pointer;
|
| 68 |
+
}
|
| 69 |
+
.error-message {
|
| 70 |
+
color: #e53e3e;
|
| 71 |
+
font-size: 0.875rem;
|
| 72 |
+
margin-top: 0.25rem;
|
| 73 |
+
}
|
| 74 |
+
</style>
|
| 75 |
+
<div class="auth-container">
|
| 76 |
+
<h1 class="auth-title">KPI Tracker Pro</h1>
|
| 77 |
+
<form id="loginForm">
|
| 78 |
+
<div class="input-group">
|
| 79 |
+
<label class="input-label" for="email">Email</label>
|
| 80 |
+
<input type="email" id="email" class="input-field" placeholder="your@email.com" required>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="input-group">
|
| 83 |
+
<label class="input-label" for="password">Password</label>
|
| 84 |
+
<input type="password" id="password" class="input-field" placeholder="••••••••" required>
|
| 85 |
+
</div>
|
| 86 |
+
<button type="submit" class="submit-btn">Sign In</button>
|
| 87 |
+
</form>
|
| 88 |
+
<div class="toggle-auth">
|
| 89 |
+
<span>Don't have an account? </span>
|
| 90 |
+
<span class="toggle-link" id="toggleRegister">Register</span>
|
| 91 |
+
</div>
|
| 92 |
+
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
| 93 |
+
</div>
|
| 94 |
+
`;
|
| 95 |
+
|
| 96 |
+
this.shadowRoot.getElementById('loginForm').addEventListener('submit', this.handleLogin.bind(this));
|
| 97 |
+
this.shadowRoot.getElementById('toggleRegister').addEventListener('click', () => {
|
| 98 |
+
this.toggleAuthMode('register');
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
handleLogin(e) {
|
| 103 |
+
e.preventDefault();
|
| 104 |
+
const email = this.shadowRoot.getElementById('email').value;
|
| 105 |
+
const password = this.shadowRoot.getElementById('password').value;
|
| 106 |
+
|
| 107 |
+
// Mock authentication - in a real app, this would be an API call
|
| 108 |
+
if (email === 'admin@kpi.com' && password === 'admin123') {
|
| 109 |
+
setCurrentUser({ email, role: 'admin', name: 'Admin User' });
|
| 110 |
+
window.location.href = '/dashboard.html';
|
| 111 |
+
} else if (email === 'manager@kpi.com' && password === 'manager123') {
|
| 112 |
+
setCurrentUser({ email, role: 'manager', name: 'Team Manager', team: 'Sales Team' });
|
| 113 |
+
window.location.href = '/dashboard.html';
|
| 114 |
+
} else if (email === 'employee@kpi.com' && password === 'employee123') {
|
| 115 |
+
setCurrentUser({ email, role: 'employee', name: 'John Doe', manager: 'manager@kpi.com' });
|
| 116 |
+
window.location.href = '/dashboard.html';
|
| 117 |
+
} else {
|
| 118 |
+
const errorElement = this.shadowRoot.getElementById('errorMessage');
|
| 119 |
+
errorElement.textContent = 'Invalid email or password';
|
| 120 |
+
errorElement.style.display = 'block';
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
toggleAuthMode(mode) {
|
| 125 |
+
const form = this.shadowRoot.getElementById('loginForm');
|
| 126 |
+
const toggleText = this.shadowRoot.querySelector('.toggle-auth span:first-child');
|
| 127 |
+
const toggleLink = this.shadowRoot.getElementById('toggleRegister');
|
| 128 |
+
const title = this.shadowRoot.querySelector('.auth-title');
|
| 129 |
+
|
| 130 |
+
if (mode === 'register') {
|
| 131 |
+
title.textContent = 'Create Account';
|
| 132 |
+
form.innerHTML = `
|
| 133 |
+
<div class="input-group">
|
| 134 |
+
<label class="input-label" for="regName">Full Name</label>
|
| 135 |
+
<input type="text" id="regName" class="input-field" placeholder="John Doe" required>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="input-group">
|
| 138 |
+
<label class="input-label" for="regEmail">Email</label>
|
| 139 |
+
<input type="email" id="regEmail" class="input-field" placeholder="your@email.com" required>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="input-group">
|
| 142 |
+
<label class="input-label" for="regPassword">Password</label>
|
| 143 |
+
<input type="password" id="regPassword" class="input-field" placeholder="••••••••" required>
|
| 144 |
+
</div>
|
| 145 |
+
<div class="input-group">
|
| 146 |
+
<label class="input-label" for="regConfirmPassword">Confirm Password</label>
|
| 147 |
+
<input type="password" id="regConfirmPassword" class="input-field" placeholder="••••••••" required>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="input-group">
|
| 150 |
+
<label class="input-label" for="regRole">Role</label>
|
| 151 |
+
<select id="regRole" class="input-field" required>
|
| 152 |
+
<option value="">Select your role</option>
|
| 153 |
+
<option value="employee">Employee</option>
|
| 154 |
+
<option value="manager">Manager</option>
|
| 155 |
+
</select>
|
| 156 |
+
</div>
|
| 157 |
+
<button type="submit" class="submit-btn">Register</button>
|
| 158 |
+
`;
|
| 159 |
+
toggleText.textContent = 'Already have an account? ';
|
| 160 |
+
toggleLink.textContent = 'Sign In';
|
| 161 |
+
toggleLink.id = 'toggleLogin';
|
| 162 |
+
toggleLink.addEventListener('click', () => {
|
| 163 |
+
this.toggleAuthMode('login');
|
| 164 |
+
});
|
| 165 |
+
form.addEventListener('submit', this.handleRegister.bind
|
index.html
CHANGED
|
@@ -1,19 +1,18 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>KPI Tracker Pro - Login</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 10 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 11 |
+
<script src="components/auth-form.js"></script>
|
| 12 |
+
</head>
|
| 13 |
+
<body class="bg-gray-50 min-h-screen flex items-center justify-center">
|
| 14 |
+
<auth-form></auth-form>
|
| 15 |
+
<script>feather.replace();</script>
|
| 16 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 17 |
+
</body>
|
| 18 |
+
</html>
|
|
|
script.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared utility functions
|
| 2 |
+
const formatDate = (dateString) => {
|
| 3 |
+
const options = { year: 'numeric', month: 'short', day: 'numeric' };
|
| 4 |
+
return new Date(dateString).toLocaleDateString(undefined, options);
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
const calculateProgress = (current, target) => {
|
| 8 |
+
return Math.min(Math.round((current / target) * 100), 100);
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
// Auth state management
|
| 12 |
+
let currentUser = null;
|
| 13 |
+
|
| 14 |
+
const setCurrentUser = (user) => {
|
| 15 |
+
currentUser = user;
|
| 16 |
+
localStorage.setItem('currentUser', JSON.stringify(user));
|
| 17 |
+
updateUIForUserRole();
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const getCurrentUser = () => {
|
| 21 |
+
if (!currentUser && localStorage.getItem('currentUser')) {
|
| 22 |
+
currentUser = JSON.parse(localStorage.getItem('currentUser'));
|
| 23 |
+
}
|
| 24 |
+
return currentUser;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
const logout = () => {
|
| 28 |
+
currentUser = null;
|
| 29 |
+
localStorage.removeItem('currentUser');
|
| 30 |
+
window.location.href = '/';
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const updateUIForUserRole = () => {
|
| 34 |
+
const user = getCurrentUser();
|
| 35 |
+
if (!user) return;
|
| 36 |
+
|
| 37 |
+
// Show/hide elements based on role
|
| 38 |
+
document.querySelectorAll('[data-role="admin"]').forEach(el => {
|
| 39 |
+
el.style.display = user.role === 'admin' ? 'block' : 'none';
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
document.querySelectorAll('[data-role="manager"]').forEach(el => {
|
| 43 |
+
el.style.display = user.role === 'manager' ? 'block' : 'none';
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
document.querySelectorAll('[data-role="employee"]').forEach(el => {
|
| 47 |
+
el.style.display = user.role === 'employee' ? 'block' : 'none';
|
| 48 |
+
});
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
// Initialize app when DOM is loaded
|
| 52 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 53 |
+
updateUIForUserRole();
|
| 54 |
+
});
|
style.css
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
margin-top: 0;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
.
|
| 19 |
-
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
.
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 2 |
+
|
| 3 |
body {
|
| 4 |
+
font-family: 'Inter', sans-serif;
|
|
|
|
| 5 |
}
|
| 6 |
|
| 7 |
+
.kpi-card {
|
| 8 |
+
transition: all 0.3s ease;
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
+
.kpi-card:hover {
|
| 12 |
+
transform: translateY(-5px);
|
| 13 |
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
+
.progress-bar {
|
| 17 |
+
transition: width 1s ease-in-out;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
+
.dashboard-grid {
|
| 21 |
+
display: grid;
|
| 22 |
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 23 |
+
gap: 1.5rem;
|
| 24 |
}
|
| 25 |
+
|
| 26 |
+
@media (max-width: 768px) {
|
| 27 |
+
.dashboard-grid {
|
| 28 |
+
grid-template-columns: 1fr;
|
| 29 |
+
}
|
| 30 |
+
}
|