diff --git a/Add_bulk_employees.js b/Add_bulk_employees.js new file mode 100644 index 0000000..af9ad1b --- /dev/null +++ b/Add_bulk_employees.js @@ -0,0 +1,135 @@ +/* ═══ CYPHER-HR Bulk Import Module ═══ */ + +const AddBulkEmployees = { + showModal() { + showModal('Import Employees', ` +
+
+

Instructions:

+
    +
  1. Download the template below.
  2. +
  3. Fill out the employee details. Email, First Name, Last Name, and Password are required.
  4. +
  5. Upload the completed .csv, .xls, or .xlsx file.
  6. +
+
+ +
+ +
+ +
+ + +
+ + + +
+ `, ` + + + `); + }, + + 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"; + } + } +}; diff --git a/CYPHER_HR_Employee_Template.csv b/CYPHER_HR_Employee_Template.csv new file mode 100644 index 0000000..65b7abf --- /dev/null +++ b/CYPHER_HR_Employee_Template.csv @@ -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 diff --git a/README.md b/README.md index 4bb2680..2574de8 100644 --- a/README.md +++ b/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. - **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. +- **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.
@@ -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.
+--- + +## 🛠️ 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. +
--- @@ -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. | | **`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. | +| **`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. |
diff --git a/admin.js b/admin.js index 575b287..38f4902 100644 --- a/admin.js +++ b/admin.js @@ -109,7 +109,10 @@ const Admin = { return `
- +
+ + +
${employees.length === 0 ? '

No employees yet. Add your first employee!

' : diff --git a/index.html b/index.html index 230de27..c48a9c4 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,9 @@ + + diff --git a/server.js b/server.js index 78b00ec..87dc354 100644 --- a/server.js +++ b/server.js @@ -183,6 +183,51 @@ app.post('/api/employees', authMiddleware, adminOnly, async (req, res) => { } 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) => { try { await query('DELETE FROM users WHERE id=$1 AND role=$2', [req.params.id, 'employee']);