NEW RELEASE
This commit is contained in:
135
Add_bulk_employees.js
Normal file
135
Add_bulk_employees.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
4
CYPHER_HR_Employee_Template.csv
Normal file
4
CYPHER_HR_Employee_Template.csv
Normal 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
|
||||||
|
29
README.md
29
README.md
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
5
admin.js
5
admin.js
@@ -109,7 +109,10 @@ 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>
|
||||||
<button class="btn btn-primary" onclick="Admin.showAddEmployee()">${icon('plus',18)} Add Employee</button>
|
<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>
|
||||||
|
</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>' :
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
45
server.js
45
server.js
@@ -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']);
|
||||||
|
|||||||
Reference in New Issue
Block a user