adding employee page. needs reports
This commit is contained in:
+15
-1
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Job Tracking Card -->
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:bg-white/70 transition-all duration-300">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -85,6 +85,20 @@
|
||||
<p class="text-sm text-gray-600 mb-4">Manage job notes and reports</p>
|
||||
<a href="notes" class="text-green-600 hover:text-green-700 font-medium text-sm">View Notes →</a>
|
||||
</div>
|
||||
|
||||
<!-- Employees Card -->
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:bg-white/70 transition-all duration-300">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2">Employees</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Manage employee records</p>
|
||||
<a href="employees" class="text-purple-600 hover:text-purple-700 font-medium text-sm">View Employees →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Job Tracking - Employees</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@latest/dist/pocketbase.umd.js"></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-indigo-50/40">
|
||||
<!-- Navigation Header -->
|
||||
<nav class="backdrop-blur-xl bg-white/70 border-b border-white/20 shadow-sm sticky top-0 z-40">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-row justify-between items-center py-4 px-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="dashboard" class="text-indigo-600 hover:text-indigo-700 font-medium text-sm sm:text-base transition-colors">← Dashboard</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 relative">
|
||||
<div class="relative">
|
||||
<button
|
||||
onclick="toggleUserDropdown()"
|
||||
class="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 flex items-center justify-center text-white font-medium text-sm hover:scale-105 focus:outline-none transition-all duration-300 shadow-lg shadow-indigo-500/30 hover:shadow-indigo-500/50"
|
||||
id="userAvatarBtn"
|
||||
>
|
||||
<span id="userInitials" class="text-xs font-semibold"></span>
|
||||
</button>
|
||||
<div
|
||||
id="userDropdown"
|
||||
class="hidden absolute right-0 mt-2 w-48 backdrop-blur-xl bg-white/80 rounded-2xl shadow-2xl border border-white/30 py-2 z-50 overflow-hidden"
|
||||
>
|
||||
<div class="px-4 py-3 border-b border-white/20">
|
||||
<p class="text-xs text-gray-500 font-medium">Signed in as</p>
|
||||
<p class="text-sm font-semibold text-gray-900 truncate mt-0.5" id="userEmail"></p>
|
||||
</div>
|
||||
<a
|
||||
href="preferences"
|
||||
class="block px-4 py-2.5 text-sm text-gray-700 hover:bg-white/40 focus:outline-none transition-colors font-medium"
|
||||
>
|
||||
Preferences
|
||||
</a>
|
||||
<button
|
||||
onclick="logout()"
|
||||
class="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50/50 focus:outline-none transition-colors font-medium"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-gray-800 mb-2">Employees</h1>
|
||||
<p class="text-sm sm:text-base text-gray-600">Manage employee records</p>
|
||||
</div>
|
||||
<button
|
||||
onclick="openEmployeeModal()"
|
||||
class="w-full sm:w-auto bg-indigo-600 text-white px-4 sm:px-6 py-2 sm:py-3 rounded-lg text-sm sm:text-base font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition"
|
||||
>
|
||||
+ Add Employee
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message Container -->
|
||||
<div id="messageContainer" class="mb-4"></div>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
id="searchInput"
|
||||
placeholder="Search by name, email, phone, or department..."
|
||||
class="w-full px-4 py-3 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div class="mb-4 flex justify-end">
|
||||
<div class="inline-flex rounded-2xl border border-white/30 backdrop-blur-xl bg-white/60 p-1 shadow-lg">
|
||||
<button
|
||||
onclick="switchView('table')"
|
||||
id="tableViewBtn"
|
||||
class="px-4 py-2 rounded-xl text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/50 backdrop-blur-sm bg-indigo-600/90 text-white shadow-lg hover:bg-indigo-700/90"
|
||||
>
|
||||
<svg class="w-5 h-5 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Table
|
||||
</button>
|
||||
<button
|
||||
onclick="switchView('card')"
|
||||
id="cardViewBtn"
|
||||
class="px-4 py-2 rounded-xl text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-indigo-500/50 text-gray-700 hover:bg-white/40 backdrop-blur-sm"
|
||||
>
|
||||
<svg class="w-5 h-5 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
|
||||
</svg>
|
||||
Cards
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employees Table View -->
|
||||
<div id="tableView" class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="backdrop-blur-sm bg-white/40">
|
||||
<tr>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">First Name</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Name</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Phone</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Voxer ID</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-3 sm:px-6 py-2 sm:py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="employeesTableBody" class="bg-white/30 divide-y divide-white/20">
|
||||
<tr id="loadingRow">
|
||||
<td colspan="7" class="px-6 py-8 text-center text-gray-500">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employees Card View -->
|
||||
<div id="cardView" class="hidden">
|
||||
<div id="employeesCardContainer" class="space-y-4">
|
||||
<div class="text-center py-8 text-gray-500">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="paginationContainer" class="mt-4 flex justify-between items-center"></div>
|
||||
</main>
|
||||
|
||||
<!-- Employee Modal -->
|
||||
<div id="employeeModal" class="hidden fixed inset-0 backdrop-blur-sm bg-black/20 overflow-y-auto z-50">
|
||||
<div class="relative top-0 sm:top-10 mx-auto p-4 sm:p-6 w-full sm:max-w-3xl shadow-2xl rounded-none sm:rounded-3xl backdrop-blur-2xl bg-white/80 border border-white/30 mb-0 sm:mb-10 min-h-screen sm:min-h-[600px]">
|
||||
<!-- Modal Header -->
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-900" id="employeeModalTitle">Add Employee</h2>
|
||||
<button
|
||||
onclick="closeEmployeeModal()"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex space-x-8 overflow-x-auto">
|
||||
<button onclick="switchEmployeeTab('overview')" id="tab-overview" class="employee-tab border-b-2 border-indigo-500 text-indigo-600 font-medium text-sm py-2 px-1 whitespace-nowrap">Overview</button>
|
||||
<button onclick="switchEmployeeTab('reports')" id="tab-reports" class="employee-tab border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 font-medium text-sm py-2 px-1 whitespace-nowrap hidden">Reports</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee Form -->
|
||||
<form id="employeeForm">
|
||||
<input type="hidden" id="employeeId" name="id">
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Overview Tab Content -->
|
||||
<div id="employee-tab-overview" class="employee-tab-content space-y-6 min-h-[500px]">
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Basic Information</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="First_Name" class="block text-sm font-medium text-gray-700 mb-1">First Name *</label>
|
||||
<input type="text" id="First_Name" name="First_Name" required class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="Last_Name" class="block text-sm font-medium text-gray-700 mb-1">Last Name *</label>
|
||||
<input type="text" id="Last_Name" name="Last_Name" required class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="Email_Address" class="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input type="email" id="Email_Address" name="Email_Address" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="Phone_Number" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
|
||||
<input type="tel" id="Phone_Number" name="Phone_Number" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="Voxer_ID" class="block text-sm font-medium text-gray-700 mb-1">Voxer ID</label>
|
||||
<input type="text" id="Voxer_ID" name="Voxer_ID" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="Status" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<input type="text" id="Status" name="Status" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Fields Section (for any extra fields) -->
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Additional Information</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" id="additionalFieldsContainer">
|
||||
<!-- Additional fields will be dynamically added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reports Tab Content -->
|
||||
<div id="employee-tab-reports" class="employee-tab-content hidden space-y-6 min-h-[500px]">
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Employee Reports</h3>
|
||||
<button
|
||||
type="button"
|
||||
onclick="openReportModal()"
|
||||
class="bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
+ Add Report
|
||||
</button>
|
||||
</div>
|
||||
<div id="reportsList" class="space-y-4">
|
||||
<p class="text-center text-gray-500 py-8">Loading reports...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Buttons -->
|
||||
<div class="flex flex-col sm:flex-row justify-end gap-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onclick="closeEmployeeModal()"
|
||||
class="w-full sm:w-auto px-6 py-2.5 backdrop-blur-sm bg-white/60 border-2 border-gray-300/60 text-gray-700 rounded-xl font-medium hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-gray-500/50 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full sm:w-auto px-6 py-2.5 backdrop-blur-sm bg-indigo-600/90 text-white rounded-xl font-medium hover:bg-indigo-700/90 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Save Employee
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Modal -->
|
||||
<div id="reportModal" class="hidden fixed inset-0 backdrop-blur-sm bg-black/20 overflow-y-auto z-50">
|
||||
<div class="relative top-0 sm:top-10 mx-auto p-4 sm:p-6 w-full sm:max-w-2xl shadow-2xl rounded-none sm:rounded-3xl backdrop-blur-2xl bg-white/80 border border-white/30 mb-0 sm:mb-10 min-h-screen sm:min-h-[400px]">
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900" id="reportModalTitle">Add Report</h2>
|
||||
<button
|
||||
onclick="closeReportModal()"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="reportForm" class="space-y-6">
|
||||
<input type="hidden" id="reportId" name="id">
|
||||
<input type="hidden" id="reportEmployeeId" name="id">
|
||||
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="reportRow" class="block text-sm font-medium text-gray-700 mb-1">Row</label>
|
||||
<input type="text" id="reportRow" name="row" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="reportEnteredBy" class="block text-sm font-medium text-gray-700 mb-1">Entered By</label>
|
||||
<input type="text" id="reportEnteredBy" name="entered_by" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="reportEnteredDate" class="block text-sm font-medium text-gray-700 mb-1">Entered Date</label>
|
||||
<input type="date" id="reportEnteredDate" name="entered_date" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="reportIdNumber" class="block text-sm font-medium text-gray-700 mb-1">ID Number</label>
|
||||
<input type="text" id="reportIdNumber" name="id_number" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="reportContent" class="block text-sm font-medium text-gray-700 mb-1">Report</label>
|
||||
<textarea id="reportContent" name="report" rows="4" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-end gap-3 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onclick="closeReportModal()"
|
||||
class="w-full sm:w-auto px-6 py-2.5 backdrop-blur-sm bg-white/60 border-2 border-gray-300/60 text-gray-700 rounded-xl font-medium hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-gray-500/50 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full sm:w-auto px-6 py-2.5 backdrop-blur-sm bg-indigo-600/90 text-white rounded-xl font-medium hover:bg-indigo-700/90 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Save Report
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/pocketbase.js"></script>
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/preferences.js"></script>
|
||||
<script src="js/employees.js"></script>
|
||||
<script src="js/employee-reports.js"></script>
|
||||
<script>
|
||||
// Check authentication
|
||||
checkAuth();
|
||||
|
||||
// Initialize dark mode
|
||||
initializeDarkMode();
|
||||
|
||||
// Set user email and initials
|
||||
const user = pb.authStore.model;
|
||||
if (user) {
|
||||
const email = user.email || '';
|
||||
document.getElementById('userEmail').textContent = email;
|
||||
|
||||
// Set initials for avatar
|
||||
const initials = email
|
||||
.split('@')[0]
|
||||
.split('.')
|
||||
.map(part => part.charAt(0).toUpperCase())
|
||||
.join('')
|
||||
.substring(0, 2) || 'U';
|
||||
document.getElementById('userInitials').textContent = initials;
|
||||
}
|
||||
|
||||
// Toggle user dropdown
|
||||
function toggleUserDropdown() {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
dropdown.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
const button = document.getElementById('userAvatarBtn');
|
||||
if (dropdown && !dropdown.contains(e.target) && !button.contains(e.target)) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Make function globally accessible
|
||||
window.toggleUserDropdown = toggleUserDropdown;
|
||||
|
||||
// Modal functions
|
||||
function openEmployeeModal(employeeId = null) {
|
||||
document.getElementById('employeeModal').classList.remove('hidden');
|
||||
if (employeeId) {
|
||||
document.getElementById('employeeId').value = employeeId;
|
||||
// Show reports tab for existing employees (do this before loading to ensure it's visible)
|
||||
const reportsTabButton = document.getElementById('tab-reports');
|
||||
if (reportsTabButton) {
|
||||
reportsTabButton.classList.remove('hidden');
|
||||
}
|
||||
loadEmployeeForEdit(employeeId);
|
||||
} else {
|
||||
document.getElementById('employeeModalTitle').textContent = 'Add Employee';
|
||||
document.getElementById('employeeForm').reset();
|
||||
document.getElementById('employeeId').value = '';
|
||||
// Hide reports tab for new employees
|
||||
const reportsTabButton = document.getElementById('tab-reports');
|
||||
if (reportsTabButton) {
|
||||
reportsTabButton.classList.add('hidden');
|
||||
}
|
||||
// Clear additional fields
|
||||
document.getElementById('additionalFieldsContainer').innerHTML = '';
|
||||
// Ensure standard fields are cleared
|
||||
document.getElementById('First_Name').value = '';
|
||||
document.getElementById('Last_Name').value = '';
|
||||
document.getElementById('Email_Address').value = '';
|
||||
document.getElementById('Phone_Number').value = '';
|
||||
document.getElementById('Voxer_ID').value = '';
|
||||
document.getElementById('Status').value = '';
|
||||
}
|
||||
// Start with overview tab
|
||||
switchEmployeeTab('overview');
|
||||
}
|
||||
|
||||
function closeEmployeeModal() {
|
||||
document.getElementById('employeeModal').classList.add('hidden');
|
||||
document.getElementById('employeeForm').reset();
|
||||
document.getElementById('employeeId').value = '';
|
||||
document.getElementById('additionalFieldsContainer').innerHTML = '';
|
||||
// Reset tab to overview
|
||||
switchEmployeeTab('overview');
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.openEmployeeModal = openEmployeeModal;
|
||||
window.closeEmployeeModal = closeEmployeeModal;
|
||||
|
||||
// Form submission
|
||||
document.getElementById('employeeForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await saveEmployee();
|
||||
});
|
||||
|
||||
// Report form submission
|
||||
document.getElementById('reportForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await saveReport();
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||
clearTimeout(window.searchTimeout);
|
||||
window.searchTimeout = setTimeout(() => {
|
||||
loadEmployees(currentPage);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Load employees on page load
|
||||
setTimeout(() => {
|
||||
// Initialize view display
|
||||
if (typeof updateViewDisplay === 'function') {
|
||||
updateViewDisplay();
|
||||
}
|
||||
loadEmployees();
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,264 @@
|
||||
// Employee Reports CRUD operations
|
||||
|
||||
const reportsCollectionName = 'Employee_Reports';
|
||||
window.currentEmployeeId = null;
|
||||
|
||||
/**
|
||||
* Load reports for a specific employee
|
||||
*/
|
||||
async function loadEmployeeReports(employeeId) {
|
||||
try {
|
||||
if (!employeeId) {
|
||||
document.getElementById('reportsList').innerHTML = '<p class="text-center text-gray-500 py-8">No employee selected</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
window.currentEmployeeId = employeeId;
|
||||
|
||||
// Filter reports by id matching the employee id (Employee_Records id)
|
||||
// The id field in Employee_Reports links to Employee_Records id
|
||||
const result = await pb.collection(reportsCollectionName).getList(1, 100, {
|
||||
filter: `id = "${employeeId}"`,
|
||||
sort: '-entered_date',
|
||||
});
|
||||
|
||||
displayReports(result.items);
|
||||
} catch (error) {
|
||||
console.error('Error loading reports:', error);
|
||||
document.getElementById('reportsList').innerHTML = '<p class="text-center text-red-500 py-8">Error loading reports. Please try again.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display reports in the reports list
|
||||
*/
|
||||
function displayReports(reports) {
|
||||
const container = document.getElementById('reportsList');
|
||||
if (!container) return;
|
||||
|
||||
if (!reports || reports.length === 0) {
|
||||
container.innerHTML = '<p class="text-center text-gray-500 py-8">No reports found. Click "Add Report" to create one.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = reports.map(report => {
|
||||
const enteredDate = report.entered_date
|
||||
? new Date(report.entered_date).toLocaleDateString()
|
||||
: '-';
|
||||
|
||||
return `
|
||||
<div class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-4">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
${report.entered_date ? `
|
||||
<span class="text-xs text-gray-500">${escapeHtml(enteredDate)}</span>
|
||||
` : ''}
|
||||
${report.entered_by ? `
|
||||
<span class="text-xs text-gray-500">Entered by: ${escapeHtml(report.entered_by)}</span>
|
||||
` : ''}
|
||||
${report.row ? `
|
||||
<span class="text-xs text-gray-500">Row: ${escapeHtml(report.row)}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
${report.report ? `
|
||||
<p class="text-sm text-gray-900 whitespace-pre-wrap">${escapeHtml(report.report)}</p>
|
||||
` : '<p class="text-sm text-gray-500 italic">No report content</p>'}
|
||||
${report.id_number ? `
|
||||
<p class="text-xs text-gray-500 mt-2">ID Number: ${escapeHtml(report.id_number)}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="flex gap-2 ml-4">
|
||||
<button
|
||||
onclick="openReportModal('${report.id}')"
|
||||
class="bg-blue-600 text-white px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onclick="deleteReport('${report.id}')"
|
||||
class="bg-red-600 text-white px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 transition"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open report modal for creating or editing
|
||||
*/
|
||||
async function openReportModal(reportId = null) {
|
||||
document.getElementById('reportModal').classList.remove('hidden');
|
||||
|
||||
if (reportId) {
|
||||
document.getElementById('reportModalTitle').textContent = 'Edit Report';
|
||||
document.getElementById('reportId').value = reportId;
|
||||
await loadReportForEdit(reportId);
|
||||
} else {
|
||||
document.getElementById('reportModalTitle').textContent = 'Add Report';
|
||||
document.getElementById('reportForm').reset();
|
||||
document.getElementById('reportId').value = '';
|
||||
// Set the employee id for linking
|
||||
document.getElementById('reportEmployeeId').value = window.currentEmployeeId || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close report modal
|
||||
*/
|
||||
function closeReportModal() {
|
||||
document.getElementById('reportModal').classList.add('hidden');
|
||||
document.getElementById('reportForm').reset();
|
||||
document.getElementById('reportId').value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load report for editing
|
||||
*/
|
||||
async function loadReportForEdit(reportId) {
|
||||
try {
|
||||
const report = await pb.collection(reportsCollectionName).getOne(reportId);
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById('reportRow').value = report.row || '';
|
||||
document.getElementById('reportEnteredBy').value = report.entered_by || '';
|
||||
document.getElementById('reportEnteredDate').value = report.entered_date
|
||||
? new Date(report.entered_date).toISOString().split('T')[0]
|
||||
: '';
|
||||
document.getElementById('reportIdNumber').value = report.id_number || '';
|
||||
document.getElementById('reportContent').value = report.report || '';
|
||||
document.getElementById('reportEmployeeId').value = report.id || window.currentEmployeeId || '';
|
||||
|
||||
return report;
|
||||
} catch (error) {
|
||||
console.error('Error loading report:', error);
|
||||
showMessage('Error loading report: ' + (error.message || 'Unknown error'), 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save report (create or update)
|
||||
*/
|
||||
async function saveReport() {
|
||||
try {
|
||||
const form = document.getElementById('reportForm');
|
||||
if (!form) {
|
||||
showMessage('Form not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
const reportId = document.getElementById('reportId')?.value;
|
||||
const employeeId = window.currentEmployeeId || document.getElementById('reportEmployeeId')?.value;
|
||||
|
||||
if (!employeeId) {
|
||||
showMessage('No employee ID found. Please save the employee first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build report object
|
||||
// Note: In PocketBase, when creating, we can't set the id field directly
|
||||
// So we'll use the id field value to link to Employee_Records
|
||||
// But we need to check if this is actually a relation field or a text field
|
||||
// For now, we'll set it as a field value
|
||||
const reportData = {};
|
||||
|
||||
// Set the id field to link to Employee_Records (this is the linking field)
|
||||
reportData.id = employeeId;
|
||||
|
||||
// Get all form fields
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key === 'id') continue; // Skip the hidden id field
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
if (key === 'entered_date' && value) {
|
||||
// Convert date to ISO string
|
||||
reportData[key] = new Date(value).toISOString();
|
||||
} else {
|
||||
reportData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get employee info to populate name fields if available
|
||||
if (employeeId && typeof loadEmployeeForEdit === 'undefined') {
|
||||
// Try to get employee data if we have access to it
|
||||
try {
|
||||
const employee = await pb.collection('Employee_Records').getOne(employeeId);
|
||||
if (employee) {
|
||||
reportData.first_name = employee.First_Name || '';
|
||||
reportData.last_name = employee.Last_Name || '';
|
||||
reportData.combined_name = `${employee.First_Name || ''} ${employee.Last_Name || ''}`.trim();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load employee data for report:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (reportId) {
|
||||
// Update existing report
|
||||
await pb.collection(reportsCollectionName).update(reportId, reportData);
|
||||
showMessage('Report updated successfully', 'success');
|
||||
} else {
|
||||
// Create new report
|
||||
await pb.collection(reportsCollectionName).create(reportData);
|
||||
showMessage('Report created successfully', 'success');
|
||||
}
|
||||
|
||||
closeReportModal();
|
||||
if (window.currentEmployeeId) {
|
||||
loadEmployeeReports(window.currentEmployeeId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving report:', error);
|
||||
const errorMsg = error.message || 'Unknown error';
|
||||
showMessage('Error saving report: ' + errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete report
|
||||
*/
|
||||
async function deleteReport(reportId) {
|
||||
if (!confirm('Are you sure you want to delete this report? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await pb.collection(reportsCollectionName).delete(reportId);
|
||||
showMessage('Report deleted successfully', 'success');
|
||||
if (window.currentEmployeeId) {
|
||||
loadEmployeeReports(window.currentEmployeeId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting report:', error);
|
||||
showMessage('Error deleting report: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.loadEmployeeReports = loadEmployeeReports;
|
||||
window.openReportModal = openReportModal;
|
||||
window.closeReportModal = closeReportModal;
|
||||
window.saveReport = saveReport;
|
||||
window.deleteReport = deleteReport;
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
// Employee CRUD operations
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = 20;
|
||||
let allEmployees = [];
|
||||
let employeesCollectionName = 'Employee_Records';
|
||||
let currentView = localStorage.getItem('employeesView') || 'table'; // 'table' or 'card'
|
||||
|
||||
// Common employee field names that we'll display (matching PocketBase schema)
|
||||
const standardFields = ['First_Name', 'Last_Name', 'Email_Address', 'Phone_Number', 'Voxer_ID', 'Status'];
|
||||
|
||||
/**
|
||||
* Load all employees with optional search filter
|
||||
*/
|
||||
async function loadEmployees(page = 1) {
|
||||
try {
|
||||
// Show loading state based on current view
|
||||
if (currentView === 'table') {
|
||||
const tbody = document.getElementById('employeesTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '<tr id="loadingRow"><td colspan="7" class="px-6 py-8 text-center text-gray-500">Loading...</td></tr>';
|
||||
}
|
||||
} else {
|
||||
const container = document.getElementById('employeesCardContainer');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">Loading...</div>';
|
||||
}
|
||||
}
|
||||
|
||||
currentPage = page;
|
||||
const searchTerm = document.getElementById('searchInput')?.value || '';
|
||||
|
||||
// Build search filter
|
||||
let searchFilter = '';
|
||||
if (searchTerm) {
|
||||
// Escape special characters for PocketBase filter
|
||||
const escapedTerm = searchTerm.replace(/"/g, '\\"');
|
||||
// Search across common text fields
|
||||
searchFilter = `First_Name ~ "${escapedTerm}" || Last_Name ~ "${escapedTerm}" || Email_Address ~ "${escapedTerm}" || Phone_Number ~ "${escapedTerm}" || Voxer_ID ~ "${escapedTerm}" || Status ~ "${escapedTerm}"`;
|
||||
}
|
||||
|
||||
const result = await pb.collection(employeesCollectionName).getList(page, itemsPerPage, {
|
||||
filter: searchFilter || undefined,
|
||||
sort: 'Last_Name',
|
||||
});
|
||||
|
||||
allEmployees = result.items;
|
||||
displayEmployees(allEmployees);
|
||||
displayPagination(result.page, result.totalPages, result.totalItems);
|
||||
|
||||
// Update view based on current view preference
|
||||
updateViewDisplay();
|
||||
} catch (error) {
|
||||
console.error('Error loading employees:', error);
|
||||
showMessage('Error loading employees: ' + (error.message || 'Unknown error'), 'error');
|
||||
if (currentView === 'table') {
|
||||
const tbody = document.getElementById('employeesTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-8 text-center text-red-500">Error loading employees. Please refresh the page.</td></tr>';
|
||||
}
|
||||
} else {
|
||||
const container = document.getElementById('employeesCardContainer');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-red-500">Error loading employees. Please refresh the page.</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display employees in table or card view
|
||||
*/
|
||||
function displayEmployees(employees) {
|
||||
if (currentView === 'table') {
|
||||
displayEmployeesTable(employees);
|
||||
} else {
|
||||
displayEmployeesCards(employees);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display employees in table view
|
||||
*/
|
||||
function displayEmployeesTable(employees) {
|
||||
const tbody = document.getElementById('employeesTableBody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!employees || employees.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-8 text-center text-gray-500">No employees found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = employees.map(employee => {
|
||||
// Use correct field names from schema
|
||||
const firstName = employee.First_Name || '-';
|
||||
const lastName = employee.Last_Name || '-';
|
||||
const email = employee.Email_Address || '-';
|
||||
const phone = employee.Phone_Number || '-';
|
||||
const voxerId = employee.Voxer_ID || '-';
|
||||
const status = employee.Status || '-';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(firstName)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(lastName)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(phone)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(email)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(voxerId)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-sm text-gray-900">${escapeHtml(status)}</td>
|
||||
<td class="px-3 sm:px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button onclick="openEmployeeModal('${employee.id}')" class="bg-green-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition">Edit</button>
|
||||
<button onclick="event.stopPropagation(); deleteEmployee('${employee.id}')" class="bg-red-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display employees in card view
|
||||
*/
|
||||
function displayEmployeesCards(employees) {
|
||||
const container = document.getElementById('employeesCardContainer');
|
||||
if (!container) return;
|
||||
|
||||
if (!employees || employees.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">No employees found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = employees.map(employee => {
|
||||
// Use correct field names from schema
|
||||
const firstName = employee.First_Name || '';
|
||||
const lastName = employee.Last_Name || '';
|
||||
const fullName = `${firstName} ${lastName}`.trim() || 'N/A';
|
||||
const email = employee.Email_Address || '-';
|
||||
const phone = employee.Phone_Number || '-';
|
||||
const voxerId = employee.Voxer_ID || '-';
|
||||
const status = employee.Status || '-';
|
||||
|
||||
return `
|
||||
<div
|
||||
onclick="openEmployeeModal('${employee.id}')"
|
||||
class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:shadow-2xl hover:border-white/50 hover:bg-white/70 cursor-pointer transition-all duration-300 hover:scale-[1.02]"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold text-gray-900">${escapeHtml(fullName)}</h3>
|
||||
${status !== '-' ? `
|
||||
<div class="mt-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
||||
${escapeHtml(status)}
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="event.stopPropagation(); openEmployeeModal('${employee.id}')" class="bg-green-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition">Edit</button>
|
||||
<button onclick="event.stopPropagation(); deleteEmployee('${employee.id}')" class="bg-red-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition">Delete</button>
|
||||
<svg class="w-5 h-5 text-gray-400 flex-shrink-0 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wide">Email</span>
|
||||
${email !== '-' ? `
|
||||
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(email)}</p>
|
||||
<a href="mailto:${escapeHtml(email)}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">Send Email</a>
|
||||
` : '<p class="text-sm font-medium text-gray-900 mt-1">-</p>'}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wide">Phone</span>
|
||||
${phone !== '-' ? `
|
||||
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(phone)}</p>
|
||||
<a href="tel:${phone.replace(/[^\d+]/g, '')}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">Call</a>
|
||||
` : '<p class="text-sm font-medium text-gray-900 mt-1">-</p>'}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wide">Voxer ID</span>
|
||||
<p class="text-sm font-medium text-gray-900 mt-1">${escapeHtml(voxerId)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display pagination controls
|
||||
*/
|
||||
function displayPagination(currentPage, totalPages, totalItems) {
|
||||
const container = document.getElementById('paginationContainer');
|
||||
if (!container) return;
|
||||
|
||||
if (totalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="flex items-center gap-2">';
|
||||
|
||||
// Previous button
|
||||
if (currentPage > 1) {
|
||||
html += `<button onclick="loadEmployees(${currentPage - 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Previous</button>`;
|
||||
}
|
||||
|
||||
// Page info
|
||||
html += `<span class="text-sm text-gray-700">Page ${currentPage} of ${totalPages} (${totalItems} total)</span>`;
|
||||
|
||||
// Next button
|
||||
if (currentPage < totalPages) {
|
||||
html += `<button onclick="loadEmployees(${currentPage + 1})" class="px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Next</button>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load employee for editing
|
||||
*/
|
||||
async function loadEmployeeForEdit(employeeId) {
|
||||
try {
|
||||
if (!employeeId) {
|
||||
showMessage('No employee ID provided', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
const employee = await pb.collection(employeesCollectionName).getOne(employeeId);
|
||||
|
||||
// Set current employee ID for reports
|
||||
window.currentEmployeeId = employeeId;
|
||||
|
||||
// Update modal header
|
||||
if (typeof updateEmployeeModalInfo === 'function') {
|
||||
updateEmployeeModalInfo(employee);
|
||||
}
|
||||
|
||||
// Show reports tab for existing employees - do this after a short delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
const reportsTabButton = document.getElementById('tab-reports');
|
||||
if (reportsTabButton) {
|
||||
reportsTabButton.classList.remove('hidden');
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Populate standard form fields using correct field names
|
||||
const firstNameField = document.getElementById('First_Name');
|
||||
if (firstNameField) firstNameField.value = employee.First_Name || '';
|
||||
|
||||
const lastNameField = document.getElementById('Last_Name');
|
||||
if (lastNameField) lastNameField.value = employee.Last_Name || '';
|
||||
|
||||
const emailField = document.getElementById('Email_Address');
|
||||
if (emailField) emailField.value = employee.Email_Address || '';
|
||||
|
||||
const phoneField = document.getElementById('Phone_Number');
|
||||
if (phoneField) phoneField.value = employee.Phone_Number || '';
|
||||
|
||||
const voxerField = document.getElementById('Voxer_ID');
|
||||
if (voxerField) voxerField.value = employee.Voxer_ID || '';
|
||||
|
||||
const statusField = document.getElementById('Status');
|
||||
if (statusField) statusField.value = employee.Status || '';
|
||||
|
||||
// Handle additional fields dynamically
|
||||
const additionalFieldsContainer = document.getElementById('additionalFieldsContainer');
|
||||
if (additionalFieldsContainer) {
|
||||
additionalFieldsContainer.innerHTML = '';
|
||||
|
||||
// Get all fields from the employee record
|
||||
const allFields = Object.keys(employee);
|
||||
const systemFields = new Set(['id', 'created', 'updated', 'collectionId', 'collectionName']);
|
||||
|
||||
// Add any fields that aren't in the standard fields
|
||||
allFields.forEach(field => {
|
||||
// Skip system fields
|
||||
if (systemFields.has(field) || field.startsWith('@')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip standard fields (already handled above)
|
||||
if (standardFields.includes(field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create input for this additional field
|
||||
if (employee[field] !== undefined) {
|
||||
const fieldLabel = field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
const fieldId = `field_${field}`;
|
||||
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.innerHTML = `
|
||||
<label for="${fieldId}" class="block text-sm font-medium text-gray-700 mb-1">${fieldLabel}</label>
|
||||
<input type="text" id="${fieldId}" name="${field}" value="${escapeHtml(String(employee[field] || ''))}" class="w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm">
|
||||
`;
|
||||
additionalFieldsContainer.appendChild(fieldDiv.firstElementChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return employee;
|
||||
} catch (error) {
|
||||
console.error('Error loading employee:', error);
|
||||
showMessage('Error loading employee: ' + (error.message || 'Unknown error'), 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save employee (create or update)
|
||||
*/
|
||||
async function saveEmployee() {
|
||||
try {
|
||||
const form = document.getElementById('employeeForm');
|
||||
if (!form) {
|
||||
showMessage('Form not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
const employeeId = document.getElementById('employeeId')?.value;
|
||||
|
||||
// Build employee object
|
||||
const employeeData = {};
|
||||
|
||||
// Get all form fields from FormData (this includes standard fields)
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (key === 'id') continue;
|
||||
// Include the value (even if empty for standard fields to allow clearing)
|
||||
if (value !== null && value !== undefined) {
|
||||
employeeData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Also get additional fields from dynamically created inputs
|
||||
const additionalFields = document.querySelectorAll('#additionalFieldsContainer input');
|
||||
additionalFields.forEach(input => {
|
||||
if (input.name && input.value !== null && input.value !== undefined) {
|
||||
employeeData[input.name] = input.value.trim();
|
||||
}
|
||||
});
|
||||
|
||||
if (employeeId) {
|
||||
// Update existing employee
|
||||
const updatedEmployee = await pb.collection(employeesCollectionName).update(employeeId, employeeData);
|
||||
const employeeName = `${updatedEmployee.First_Name || ''} ${updatedEmployee.Last_Name || ''}`.trim() || 'Employee';
|
||||
showMessage(`${employeeName} updated successfully`, 'success');
|
||||
} else {
|
||||
// Create new employee
|
||||
const newEmployee = await pb.collection(employeesCollectionName).create(employeeData);
|
||||
const employeeName = `${newEmployee.First_Name || ''} ${newEmployee.Last_Name || ''}`.trim() || 'Employee';
|
||||
showMessage(`${employeeName} created successfully`, 'success');
|
||||
}
|
||||
|
||||
if (typeof closeEmployeeModal === 'function') {
|
||||
closeEmployeeModal();
|
||||
}
|
||||
loadEmployees(currentPage);
|
||||
} catch (error) {
|
||||
console.error('Error saving employee:', error);
|
||||
const errorMsg = error.message || 'Unknown error';
|
||||
showMessage('Error saving employee: ' + errorMsg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete employee
|
||||
*/
|
||||
async function deleteEmployee(employeeId) {
|
||||
// Find the employee name for the confirmation message
|
||||
const employee = allEmployees.find(emp => emp.id === employeeId);
|
||||
const employeeName = employee
|
||||
? `${employee.First_Name || ''} ${employee.Last_Name || ''}`.trim() || 'this employee'
|
||||
: 'this employee';
|
||||
|
||||
if (!confirm(`Are you sure you want to delete ${employeeName}? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await pb.collection(employeesCollectionName).delete(employeeId);
|
||||
showMessage(`Employee ${employeeName} deleted successfully`, 'success');
|
||||
// Reload employees with current view state
|
||||
loadEmployees(currentPage);
|
||||
} catch (error) {
|
||||
console.error('Error deleting employee:', error);
|
||||
showMessage('Error deleting employee: ' + (error.message || 'Unknown error'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show message to user
|
||||
*/
|
||||
function showMessage(message, type = 'success') {
|
||||
const container = document.getElementById('messageContainer');
|
||||
if (!container) return;
|
||||
|
||||
const bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="${bgColor} border px-4 py-3 rounded-lg text-sm">
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
container.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between table and card view
|
||||
*/
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
localStorage.setItem('employeesView', view);
|
||||
updateViewDisplay();
|
||||
displayEmployees(allEmployees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update view display based on current view preference
|
||||
*/
|
||||
function updateViewDisplay() {
|
||||
const tableView = document.getElementById('tableView');
|
||||
const cardView = document.getElementById('cardView');
|
||||
const tableViewBtn = document.getElementById('tableViewBtn');
|
||||
const cardViewBtn = document.getElementById('cardViewBtn');
|
||||
|
||||
if (currentView === 'table') {
|
||||
// Show table, hide card
|
||||
if (tableView) tableView.classList.remove('hidden');
|
||||
if (cardView) cardView.classList.add('hidden');
|
||||
|
||||
// Table button: active (indigo with shadow)
|
||||
if (tableViewBtn) {
|
||||
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
|
||||
tableViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
||||
}
|
||||
// Card button: inactive (transparent)
|
||||
if (cardViewBtn) {
|
||||
cardViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
||||
cardViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
|
||||
}
|
||||
} else {
|
||||
// Show card, hide table
|
||||
if (tableView) tableView.classList.add('hidden');
|
||||
if (cardView) cardView.classList.remove('hidden');
|
||||
|
||||
// Table button: inactive (transparent)
|
||||
if (tableViewBtn) {
|
||||
tableViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
||||
tableViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
|
||||
}
|
||||
// Card button: active (indigo with shadow)
|
||||
if (cardViewBtn) {
|
||||
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
|
||||
cardViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch employee modal tab
|
||||
*/
|
||||
function switchEmployeeTab(tabName) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll('.employee-tab-content').forEach(tab => {
|
||||
tab.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Remove active state from all tabs
|
||||
document.querySelectorAll('.employee-tab').forEach(btn => {
|
||||
btn.classList.remove('border-indigo-500', 'text-indigo-600');
|
||||
btn.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
|
||||
// Show selected tab content
|
||||
const selectedTab = document.getElementById(`employee-tab-${tabName}`);
|
||||
if (selectedTab) {
|
||||
selectedTab.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Activate selected tab button
|
||||
const selectedButton = document.getElementById(`tab-${tabName}`);
|
||||
if (selectedButton) {
|
||||
selectedButton.classList.add('border-indigo-500', 'text-indigo-600');
|
||||
selectedButton.classList.remove('border-transparent', 'text-gray-500');
|
||||
}
|
||||
|
||||
// Load reports if reports tab is selected
|
||||
if (tabName === 'reports' && window.currentEmployeeId && typeof loadEmployeeReports === 'function') {
|
||||
setTimeout(() => {
|
||||
loadEmployeeReports(window.currentEmployeeId);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update employee modal header info
|
||||
*/
|
||||
function updateEmployeeModalInfo(employee) {
|
||||
const firstName = employee.First_Name || '';
|
||||
const lastName = employee.Last_Name || '';
|
||||
const fullName = `${firstName} ${lastName}`.trim() || 'Employee';
|
||||
document.getElementById('employeeModalTitle').textContent = fullName;
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.loadEmployees = loadEmployees;
|
||||
window.loadEmployeeForEdit = loadEmployeeForEdit;
|
||||
window.saveEmployee = saveEmployee;
|
||||
window.deleteEmployee = deleteEmployee;
|
||||
window.switchView = switchView;
|
||||
window.updateViewDisplay = updateViewDisplay;
|
||||
window.switchEmployeeTab = switchEmployeeTab;
|
||||
Reference in New Issue
Block a user