NEW RELEASE

This commit is contained in:
2026-04-25 15:57:51 +05:00
parent 6de5e63841
commit cbe82556d3
6 changed files with 219 additions and 1 deletions

135
Add_bulk_employees.js Normal file
View File

@@ -0,0 +1,135 @@
/* ═══ CYPHER-HR Bulk Import Module ═══ */
const AddBulkEmployees = {
showModal() {
showModal('Import Employees', `
<div class="bulk-import-container">
<div class="import-instructions" style="background:var(--bg3); padding:16px; border-radius:var(--radius); margin-bottom:20px; text-align:left;">
<h4 style="margin-top:0;">Instructions:</h4>
<ol style="margin-bottom:0; padding-left:20px; font-size:0.9rem;">
<li>Download the template below.</li>
<li>Fill out the employee details. <b>Email, First Name, Last Name, and Password are required.</b></li>
<li>Upload the completed <code>.csv</code>, <code>.xls</code>, or <code>.xlsx</code> file.</li>
</ol>
</div>
<div class="form-group" style="text-align: center; margin-bottom: 24px;">
<button type="button" class="btn btn-outline" onclick="AddBulkEmployees.downloadTemplate()">
${icon('download', 18)} Download Template
</button>
</div>
<div class="form-group">
<label>Upload Completed File</label>
<input type="file" id="bulkUploadFile" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" style="padding:10px; border:1px dashed var(--border2); border-radius:var(--radius); width:100%; cursor:pointer;">
</div>
<div id="bulkError" class="form-error" style="display:none; text-align:left;"></div>
<div id="bulkSuccess" class="form-success" style="display:none; color:var(--success); margin-top:10px; font-weight:600; text-align:center;"></div>
</div>
`, `
<button class="btn btn-outline" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" id="btnProcessImport" onclick="AddBulkEmployees.processFile()">Process Import</button>
`);
},
downloadTemplate() {
const csvContent = "first_name,last_name,email,password,department,position,phone\nJohn,Doe,john.doe@company.com,password123,IT,Developer,1234567890\nJane,Smith,jane.smith@company.com,password123,HR,Manager,0987654321";
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", "CYPHER_HR_Employee_Template.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
async processFile() {
const fileInput = document.getElementById('bulkUploadFile');
const errEl = document.getElementById('bulkError');
const succEl = document.getElementById('bulkSuccess');
const btn = document.getElementById('btnProcessImport');
errEl.style.display = 'none';
succEl.style.display = 'none';
if (!fileInput.files || fileInput.files.length === 0) {
errEl.textContent = "Please select a file to upload.";
errEl.style.display = 'block';
return;
}
const file = fileInput.files[0];
// Check if SheetJS is loaded
if (typeof XLSX === 'undefined') {
errEl.textContent = "Excel parser library not loaded. Please refresh the page.";
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.innerHTML = `${icon('clock', 18)} Processing...`;
try {
const data = await file.arrayBuffer();
const workbook = XLSX.read(data);
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const json = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
if (json.length === 0) throw new Error("The uploaded file is empty.");
// Normalize keys and validate
const payload = json.map((row, index) => {
// Find keys case-insensitively
const getVal = (keyStr) => {
const key = Object.keys(row).find(k => k.toLowerCase().replace(/ /g, '_') === keyStr);
return key ? row[key] : '';
};
const emp = {
first_name: getVal('first_name'),
last_name: getVal('last_name'),
email: getVal('email'),
password: getVal('password'),
department: getVal('department'),
position: getVal('position'),
phone: getVal('phone')
};
if (!emp.first_name || !emp.last_name || !emp.email || !emp.password) {
throw new Error(`Row ${index + 2} is missing required fields (First Name, Last Name, Email, or Password).`);
}
return emp;
});
// Send to backend
const response = await App.api('/api/employees/bulk', {
method: 'POST',
body: JSON.stringify({ employees: payload })
});
succEl.textContent = `Successfully imported ${response.inserted} employees. Skipped ${response.skipped} duplicates.`;
succEl.style.display = 'block';
// Refresh the grid underneath if Admin is on the employees view
if (typeof Admin !== 'undefined' && document.getElementById('employeesGrid')) {
const updatedGridHTML = await Admin.employeesContent();
const contentArea = document.getElementById('contentArea');
if (contentArea) contentArea.innerHTML = updatedGridHTML;
}
setTimeout(closeModal, 3000);
} catch (err) {
errEl.textContent = err.message || "Failed to process file.";
errEl.style.display = 'block';
} finally {
btn.disabled = false;
btn.textContent = "Process Import";
}
}
};

View File

@@ -0,0 +1,4 @@
first_name,last_name,email,password,department,position,phone
John,Doe,john.doe@company.com,password123,IT,Developer,1234567890
Jane,Smith,jane.smith@company.com,password123,HR,Manager,0987654321
Alex,Johnson,alex.j@company.com,password123,Sales,Executive,5551234567
1 first_name last_name email password department position phone
2 John Doe john.doe@company.com password123 IT Developer 1234567890
3 Jane Smith jane.smith@company.com password123 HR Manager 0987654321
4 Alex Johnson alex.j@company.com password123 Sales Executive 5551234567

View File

@@ -68,6 +68,7 @@ CYPHER-HR is built as an enterprise-grade Single Page Application (SPA), rivalin
- **Granular Leave Controls:** Create custom leave policies (e.g., Sick, Casual, Maternity) and set exact monthly limits. - **Granular Leave Controls:** Create custom leave policies (e.g., Sick, Casual, Maternity) and set exact monthly limits.
- **Direct Allowance Adjustments:** Manually increment or decrement individual employee leave balances when necessary. - **Direct Allowance Adjustments:** Manually increment or decrement individual employee leave balances when necessary.
- **Automated Monthly Resets:** A built-in backend Cron Job automatically refreshes all enabled leave allowances at midnight on the 1st of every month. - **Automated Monthly Resets:** A built-in backend Cron Job automatically refreshes all enabled leave allowances at midnight on the 1st of every month.
- **Bulk Employee Import:** Need to onboard 500 people? Upload an entire roster at once via Excel (`.xls`, `.xlsx`) or `.csv`. A pre-formatted `CYPHER_HR_Employee_Template.csv` is included directly in the repository for quick, seamless mass-onboarding. Files are parsed instantly in the browser using the blazing-fast SheetJS library.
</details> </details>
<details open> <details open>
@@ -89,6 +90,33 @@ CYPHER-HR is built as an enterprise-grade Single Page Application (SPA), rivalin
- **Custom Date Picker Engine:** Bypasses ugly native browser inputs with a fully custom, interactive calendar dropdown built in Vanilla JS. - **Custom Date Picker Engine:** Bypasses ugly native browser inputs with a fully custom, interactive calendar dropdown built in Vanilla JS.
</details> </details>
---
## 🛠️ Feature Deep-Dive
We don't just list features; we build them right. Here is exactly how some of the most advanced capabilities of CYPHER-HR work under the hood.
### 📈 Bulk Employee Onboarding (Excel & CSV)
Manually typing out 100 new hires is a nightmare. CYPHER-HR includes a **Mass Import Engine**.
1. **The Template:** Find the `CYPHER_HR_Employee_Template.csv` file located directly in the repository root. Open it in Microsoft Excel, Google Sheets, or Apple Numbers.
2. **The Formatting:** Just fill out `first_name`, `last_name`, `email`, and `password`. The system is smart enough to handle missing optional fields (like department or phone).
3. **The Engine:** Go to the Admin Dashboard -> Employees -> Click **Import Employees**. Upload your spreadsheet.
4. **Client-Side Parsing:** Using the blazing-fast `SheetJS` library via CDN, the dashboard parses the entire Excel file directly in your browser. This means zero lag and zero heavy lifting for your backend server!
5. **Smart Skip:** If you accidentally upload an employee who already exists, the backend intelligently skips the duplicate email without crashing the rest of the batch.
### 🌗 Global Dark Mode
A true corporate system respects your eyes during late-night shifts.
- **Not just a theme:** The dark mode toggle actively shifts the entire CSS variable hierarchy (`--bg`, `--surface`, `--text`).
- **Persistent State:** Your preference is automatically saved to your browser's `localStorage`. Next time you boot up the dashboard, it remembers if you prefer light or dark mode.
### 📄 PDF Reporting Engine
Instead of just handing you raw data, CYPHER-HR creates beautiful, structured PDF documents.
- **Powered by jsPDF:** Using `jsPDF` and `jspdf-autotable`, the frontend takes your filtered leave requests and dynamically draws a professional PDF document.
- **Custom Headers:** The PDF automatically injects the current timestamp, the applied filters, and draws corporate-style tables that you can immediately send to payroll or management.
### 🌍 Local Area Network (LAN) Ready
CYPHER-HR isn't just stuck on `localhost`. The backend Express server is explicitly bound to `0.0.0.0`, meaning it is instantly accessible to anyone on your local network out-of-the-box. Spin it up on a spare office computer, and your entire HR department can securely connect via the host's local IP address without any complex reverse-proxy setup.
<br /> <br />
--- ---
@@ -145,6 +173,7 @@ Want to modify the code? Here is exactly what every file in the project does:
| **`employee.js`** | The employee portal. Renders personalized stat cards, leave allowance progress bars, and handles the logic for submitting new time-off requests. | | **`employee.js`** | The employee portal. Renders personalized stat cards, leave allowance progress bars, and handles the logic for submitting new time-off requests. |
| **`reports.js`** | The reporting UI controller. It manages the complex filtering forms, queries the API with URL parameters, and renders the data tables. | | **`reports.js`** | The reporting UI controller. It manages the complex filtering forms, queries the API with URL parameters, and renders the data tables. |
| **`pdf_reports.js`** | The specialized PDF export engine. It takes JSON data from `reports.js`, configures document layouts, creates custom headers, and utilizes `jspdf-autotable` to draw perfectly aligned tables before initiating a browser download. | | **`pdf_reports.js`** | The specialized PDF export engine. It takes JSON data from `reports.js`, configures document layouts, creates custom headers, and utilizes `jspdf-autotable` to draw perfectly aligned tables before initiating a browser download. |
| **`Add_bulk_employees.js`** | The Excel/CSV parsing engine. Hooks into the Admin portal, manages the file upload modal, parses spreadsheets locally using SheetJS, and shoots JSON payloads to the backend for mass insertion. |
<br /> <br />

View File

@@ -109,8 +109,11 @@ const Admin = {
return ` return `
<div class="content-header"> <div class="content-header">
<div class="search-box">${icon('search',18)}<input type="text" placeholder="Search employees..." oninput="Admin.filterEmployees(this.value)"></div> <div class="search-box">${icon('search',18)}<input type="text" placeholder="Search employees..." oninput="Admin.filterEmployees(this.value)"></div>
<div style="display:flex; gap:10px;">
<button class="btn btn-outline" onclick="AddBulkEmployees.showModal()">${icon('download',18)} Import Employees</button>
<button class="btn btn-primary" onclick="Admin.showAddEmployee()">${icon('plus',18)} Add Employee</button> <button class="btn btn-primary" onclick="Admin.showAddEmployee()">${icon('plus',18)} Add Employee</button>
</div> </div>
</div>
<div class="employees-grid" id="employeesGrid"> <div class="employees-grid" id="employeesGrid">
${employees.length === 0 ? '<p class="empty-state">No employees yet. Add your first employee!</p>' : ${employees.length === 0 ? '<p class="empty-state">No employees yet. Add your first employee!</p>' :
employees.map(emp => ` employees.map(emp => `

View File

@@ -18,7 +18,9 @@
<script src="leaves.js"></script> <script src="leaves.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
<script src="pdf_reports.js"></script> <script src="pdf_reports.js"></script>
<script src="reports.js"></script> <script src="reports.js"></script>
<script src="Add_bulk_employees.js"></script>
</body> </body>
</html> </html>

View File

@@ -183,6 +183,51 @@ app.post('/api/employees', authMiddleware, adminOnly, async (req, res) => {
} catch (err) { res.status(500).json({ error: err.message }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
app.post('/api/employees/bulk', authMiddleware, adminOnly, async (req, res) => {
try {
const { employees } = req.body;
if (!Array.isArray(employees)) return res.status(400).json({ error: 'Invalid payload format' });
let inserted = 0;
let skipped = 0;
const colors = ['#6366f1','#8b5cf6','#ec4899','#f43f5e','#14b8a6','#f97316','#06b6d4','#84cc16','#eab308'];
const now = new Date();
const policies = await query('SELECT id,monthly_limit FROM leave_policies WHERE is_enabled=true');
for (const emp of employees) {
if (!emp.email || !emp.password || !emp.first_name || !emp.last_name) {
skipped++;
continue;
}
const exists = await query('SELECT id FROM users WHERE email=$1', [emp.email.toLowerCase()]);
if (exists.rowCount > 0) {
skipped++;
continue;
}
const hashed = await bcrypt.hash(emp.password, 12);
const color = colors[Math.floor(Math.random() * colors.length)];
const r = await query(
"INSERT INTO users(email,password,first_name,last_name,role,department,position,phone,avatar_color) VALUES($1,$2,$3,$4,'employee',$5,$6,$7,$8) RETURNING id",
[emp.email.toLowerCase(), hashed, emp.first_name, emp.last_name, emp.department || null, emp.position || null, emp.phone || null, color]
);
const userId = r.rows[0].id;
for (const p of policies.rows) {
await query('INSERT INTO leave_balances(user_id,leave_policy_id,balance,month,year) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING',
[userId, p.id, p.monthly_limit, now.getMonth()+1, now.getFullYear()]);
}
inserted++;
}
res.json({ inserted, skipped });
} catch (err) { res.status(500).json({ error: err.message }); }
});
app.delete('/api/employees/:id', authMiddleware, adminOnly, async (req, res) => { app.delete('/api/employees/:id', authMiddleware, adminOnly, async (req, res) => {
try { try {
await query('DELETE FROM users WHERE id=$1 AND role=$2', [req.params.id, 'employee']); await query('DELETE FROM users WHERE id=$1 AND role=$2', [req.params.id, 'employee']);