diff --git a/map/Instuctions.md b/map/Instuctions.md
index 36dd0e0..c8666a2 100644
--- a/map/Instuctions.md
+++ b/map/Instuctions.md
@@ -36,6 +36,7 @@ Welcome to the Map project! This application is a canvassing tool for political
- **Error handling.** Always provide user feedback for errors (both backend and frontend).
- **Environment variables.** Use `.env` for secrets/config, never hardcode sensitive data.
- **Testing.** Test new features locally and ensure they do not break existing functionality.
+- **Pagination** Use pagination for API endpoints returning large datasets to avoid performance issues. For example, getAll should be getAllPaginated
## How to Add a Feature
diff --git a/map/app/controllers/cutsController.js b/map/app/controllers/cutsController.js
index 64f434a..117d548 100644
--- a/map/app/controllers/cutsController.js
+++ b/map/app/controllers/cutsController.js
@@ -313,8 +313,8 @@ class CutsController {
logger.info(`Fetching public cuts from table ID: ${config.nocodb.cutsSheetId}`);
- // Use the same pattern as getAll method that's known to work
- const response = await nocodbService.getAll(
+ // Use getAllPaginated to get ALL cuts, not just first page
+ const response = await nocodbService.getAllPaginated(
config.nocodb.cutsSheetId
);
diff --git a/map/app/public/admin.html b/map/app/public/admin.html
index 89cf465..d636959 100644
--- a/map/app/public/admin.html
+++ b/map/app/public/admin.html
@@ -625,18 +625,42 @@
-
-
diff --git a/map/app/public/css/admin/cuts-shifts.css b/map/app/public/css/admin/cuts-shifts.css
index d0c4588..b2397f4 100644
--- a/map/app/public/css/admin/cuts-shifts.css
+++ b/map/app/public/css/admin/cuts-shifts.css
@@ -267,6 +267,10 @@
justify-content: space-between;
align-items: center;
position: relative;
+ min-width: 0;
+ overflow: hidden;
+ flex-wrap: wrap;
+ gap: 10px;
}
.shift-admin-item h4 {
@@ -333,6 +337,16 @@
.shift-actions {
display: flex;
gap: 10px;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+
+.shift-actions .btn {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 80px;
+ flex: 0 0 auto;
}
.manage-volunteers-btn {
diff --git a/map/app/public/css/admin/responsive.css b/map/app/public/css/admin/responsive.css
index 35f02cf..b201094 100644
--- a/map/app/public/css/admin/responsive.css
+++ b/map/app/public/css/admin/responsive.css
@@ -370,82 +370,103 @@
margin-left: -408px;
}
- /* Users table mobile card layout */
- .users-table {
- font-size: var(--font-size-base);
- min-width: auto;
- width: 100%;
- table-layout: auto;
- border-collapse: collapse;
- display: block;
+ /* Users cards mobile layout */
+ .users-cards-container {
+ grid-template-columns: 1fr;
+ gap: 12px;
}
- .users-table thead {
- display: none;
- }
-
- .users-table tbody,
- .users-table tr,
- .users-table td {
- display: block;
- width: 100% !important;
- box-sizing: border-box;
- }
-
- .users-table tr {
- margin-bottom: 15px;
- border: 1px solid #e0e0e0;
- border-radius: var(--border-radius);
- padding: 10px;
+ .user-card {
+ padding: 12px;
}
- .users-table td {
- padding: 8px 0;
- border: none;
- display: flex;
- align-items: center;
- word-wrap: break-word;
- overflow-wrap: break-word;
+ .user-card-header {
+ /* Keep horizontal layout on mobile to keep user type next to name/email */
+ flex-direction: row;
+ align-items: flex-start;
+ gap: 8px;
}
-
- .users-table td::before {
- content: attr(data-label);
- font-weight: bold;
- width: 80px;
- min-width: 80px;
- margin-right: 10px;
- color: #555;
+
+ .user-role-badge {
+ align-items: flex-end;
+ flex-shrink: 0;
}
.user-actions {
- flex-direction: column;
- gap: 8px;
- width: 100%;
+ /* Keep all buttons in one row on mobile */
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
}
.user-communication-actions {
- justify-content: center;
- gap: 8px;
+ gap: 4px;
}
.user-communication-actions .btn {
- min-width: 40px;
- padding: 8px 12px;
- font-size: 16px;
+ min-width: 36px;
+ padding: 6px 8px;
+ font-size: 14px;
}
.user-admin-actions {
- flex-direction: column;
- gap: 6px;
+ /* Keep horizontal layout */
+ flex-direction: row;
+ gap: 4px;
+ margin-left: auto;
}
.user-admin-actions .btn {
font-size: var(--font-size-xs);
- padding: 8px 10px;
+ padding: 6px 8px;
+ white-space: nowrap;
+ }
+
+ .send-login-btn {
+ font-size: 11px !important;
+ padding: 6px 6px !important;
+ }
+
+ /* Mobile user info adjustments */
+ .user-info-primary {
+ min-width: 0;
+ flex: 1;
+ }
+
+ .user-name {
+ font-size: 16px !important;
+ margin-bottom: 2px !important;
+ }
+
+ .user-email {
+ font-size: 13px !important;
+ }
+
+ .user-details-compact {
+ font-size: 12px !important;
+ margin: 2px 0 !important;
+ }
+
+ /* Mobile search interface */
+ .users-header-actions {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 12px;
+ }
+
+ .users-search-container {
+ min-width: auto;
width: 100%;
- text-align: center;
- min-height: 36px;
- line-height: 1.3;
+ }
+
+ .users-filter-container {
+ flex-wrap: wrap;
+ }
+
+ #email-all-users-btn {
+ width: 100%;
+ justify-content: center;
}
.user-form .form-actions {
@@ -611,6 +632,51 @@
}
}
+/* Medium Screen Optimization (900px - 1400px) - Cards layout responsive */
+@media (max-width: 1400px) and (min-width: 900px) {
+ .users-cards-container {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 18px;
+ }
+
+ .user-card {
+ padding: 18px;
+ }
+
+ .user-admin-actions .btn {
+ font-size: 11px;
+ padding: 6px 10px;
+ }
+
+ /* Shifts responsive fixes for medium screens */
+ .shifts-list {
+ overflow-x: auto;
+ padding: 20px;
+ }
+
+ .shift-admin-item {
+ min-width: 0;
+ overflow: hidden;
+ flex-wrap: wrap;
+ }
+
+ .shift-actions {
+ gap: 6px;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ width: 100%;
+ min-width: 250px;
+ }
+
+ .shift-actions .btn {
+ font-size: 11px;
+ padding: 5px 8px;
+ min-width: 70px;
+ flex: 0 0 auto;
+ white-space: nowrap;
+ }
+}
+
/* Tablet Responsive (768px - 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
.admin-sidebar {
@@ -643,13 +709,72 @@
padding: var(--padding-base);
}
- .users-table {
- font-size: var(--font-size-sm);
+ .users-cards-container {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
}
- .users-table th,
- .users-table td {
- padding: 10px 8px;
+ .user-card {
+ padding: 15px;
+ }
+
+ .user-name {
+ font-size: var(--font-size-base);
+ }
+
+ /* Users search responsive */
+ .users-list-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 15px;
+ }
+
+ .users-header-actions {
+ width: 100%;
+ justify-content: space-between;
+ flex-direction: row-reverse;
+ }
+
+ .users-search-container {
+ min-width: 200px;
+ flex: 1;
+ }
+
+ /* Shifts responsive for tablet screens */
+ .shift-admin-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .shift-actions {
+ width: 100%;
+ justify-content: flex-start;
+ gap: 8px;
+ }
+
+ .shift-actions .btn {
+ font-size: 12px;
+ padding: 6px 10px;
+ min-width: 85px;
+ }
+}
+
+/* Medium Desktop (1024px - 1200px) - Ensure table actions are visible */
+@media (max-width: 1400px) and (min-width: 1025px) {
+ .users-table {
+ min-width: 800px;
+ }
+
+ .user-admin-actions .btn {
+ font-size: 10px;
+ padding: 5px 6px;
+ min-width: 75px;
+ }
+
+ .users-table th:nth-child(6),
+ .users-table td:nth-child(6) {
+ min-width: 200px;
}
}
@@ -732,74 +857,47 @@
}
}
-/* Responsive Table for Very Small Screens */
+/* Responsive Cards for Very Small Screens */
@media (max-width: 640px) {
- .users-table {
- border: 0;
+ .users-cards-container {
+ gap: 12px;
}
- .users-table thead {
- border: none;
- clip: rect(0 0 0 0);
- height: 1px;
- margin: -1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
+ .user-card {
+ padding: 12px;
}
- .users-table tr {
- border-bottom: 3px solid #ddd;
- display: block;
- margin-bottom: 10px;
- padding: 10px;
- background: #f8f9fa;
- border-radius: var(--border-radius);
+ .user-name {
+ font-size: var(--font-size-base);
}
- .users-table td {
- border: none;
- border-bottom: 1px solid #eee;
- display: block;
- font-size: var(--font-size-sm);
- text-align: right;
- padding: 8px 10px;
+ .user-email {
+ font-size: 12px;
}
- .users-table td::before {
- content: attr(data-label);
- float: left;
- font-weight: bold;
- text-transform: uppercase;
- font-size: 11px;
- color: #666;
- }
-
- .users-table td:last-child {
- border-bottom: 0;
- }
-
- .user-actions {
- justify-content: flex-end;
- margin-top: 8px;
- gap: 6px;
- }
-
- .user-communication-actions {
- margin-bottom: 6px;
+ .user-phone,
+ .user-created {
+ font-size: 12px;
}
.user-communication-actions .btn {
- min-width: 32px;
- padding: 6px 8px;
+ min-width: 38px;
+ padding: 8px 10px;
+ font-size: 14px;
}
.user-admin-actions .btn {
- width: auto !important;
- min-width: 80px;
- flex: none;
- font-size: 10px;
- padding: 6px 8px;
+ font-size: 12px;
+ padding: 8px 12px;
+ min-height: 36px;
+ }
+
+ /* Very small screens search interface */
+ .users-search-input {
+ font-size: 16px; /* Prevent zoom on iOS */
+ }
+
+ .users-filter-select {
+ font-size: 14px;
}
}
diff --git a/map/app/public/css/admin/user-management.css b/map/app/public/css/admin/user-management.css
index bbc334a..ba33c0d 100644
--- a/map/app/public/css/admin/user-management.css
+++ b/map/app/public/css/admin/user-management.css
@@ -36,39 +36,101 @@
font-size: var(--font-size-xl);
}
-/* Users Table */
-.users-table {
- width: 100%;
- border-collapse: collapse;
- margin-top: 15px;
- font-size: var(--font-size-base);
- table-layout: auto;
- min-width: 600px;
- box-sizing: border-box;
+/* Users Cards Layout */
+.users-cards-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 10px;
+ margin-top: 10px;
}
-.users-table th,
-.users-table td {
- padding: 12px;
- text-align: left;
- border-bottom: 1px solid #e0e0e0;
+.user-card {
+ background: white;
+ border: 1px solid #e0e0e0;
+ border-radius: var(--border-radius);
+ padding: 10px;
+ box-shadow: var(--shadow-sm);
+ transition: all 0.2s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
}
-.users-table th {
- background-color: #f8f9fa;
+.user-card:hover {
+ box-shadow: var(--shadow-md);
+ border-color: var(--primary-color);
+}
+
+.user-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 6px;
+}
+
+.user-info-primary {
+ flex: 1;
+ min-width: 0;
+}
+
+.user-name {
+ font-size: var(--font-size-lg);
font-weight: 600;
color: var(--dark-color);
+ margin: 0 0 1px 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.user-email {
font-size: var(--font-size-sm);
- text-transform: uppercase;
- letter-spacing: 0.5px;
+ color: #666;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.users-table tbody tr {
- transition: background-color 0.2s;
+.user-role-badge {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
}
-.users-table tbody tr:hover {
- background-color: #f8f9fa;
+.user-card-body {
+ padding-top: 2px;
+}
+
+.user-details {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.user-details-compact {
+ font-size: var(--font-size-sm);
+ color: #666;
+ margin: 0;
+ padding: 0;
+ line-height: 1.3;
+}
+
+.user-phone,
+.user-created {
+ font-size: var(--font-size-sm);
+ color: #666;
+ margin: 0;
+ padding: 1px 0;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ line-height: 1.2;
+}
+
+.user-card-footer {
+ padding-top: 2px;
}
/* User Role Badges */
@@ -103,11 +165,11 @@
/* User Expiration Information */
.expiration-info {
display: inline-block;
- margin-left: 8px;
- font-size: 0.8em;
+ margin-left: 6px;
+ font-size: 0.75em;
color: #666;
background: #f5f5f5;
- padding: 2px 6px;
+ padding: 1px 4px;
border-radius: 3px;
}
@@ -116,6 +178,17 @@
font-weight: bold;
}
+.user-card.expires-soon {
+ background-color: #fff3cd;
+ border-left: 4px solid var(--warning-color);
+}
+
+.user-card.expired {
+ background-color: #f8d7da;
+ border-left: 4px solid var(--error-color);
+ opacity: 0.7;
+}
+
.expires-soon {
background-color: #fff3cd;
border-left: 4px solid var(--warning-color);
@@ -127,30 +200,40 @@
opacity: 0.7;
}
-/* User Actions */
+/* User Actions - Card Layout */
.user-actions {
display: flex;
- flex-direction: column;
- gap: 8px;
+ gap: 4px;
+ width: 100%;
+ align-items: center;
}
.user-communication-actions {
display: flex;
- gap: 4px;
- justify-content: center;
+ gap: 3px;
}
.user-admin-actions {
display: flex;
- gap: 8px;
- justify-content: center;
+ gap: 4px;
+ margin-left: auto;
}
.user-actions .btn {
- padding: 6px 12px;
+ padding: 4px 8px;
font-size: var(--font-size-xs);
border-radius: var(--border-radius);
font-weight: 500;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ transition: all 0.2s ease;
+}
+
+.user-admin-actions .btn {
+ font-size: var(--font-size-xs);
+ padding: 4px 8px;
+ line-height: 1.3;
}
.user-communication-actions .btn {
@@ -202,19 +285,138 @@
pointer-events: none;
}
+/* Special styling for delete button with trash icon */
+.delete-user-btn {
+ background: #dc3545 !important;
+ color: white !important;
+ border: 1px solid #dc3545 !important;
+ min-width: 28px !important;
+ width: 28px !important;
+ height: 28px !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ font-size: 12px !important;
+ padding: 0 !important;
+ flex-shrink: 0 !important;
+}
+
+.delete-user-btn:hover {
+ background: #c82333 !important;
+ border-color: #bd2130 !important;
+}
+
/* Users List Header */
.users-list-header {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: flex-start;
margin-bottom: var(--padding-base);
- padding-bottom: 10px;
+ padding-bottom: 15px;
border-bottom: 2px solid #eee;
+ gap: 20px;
+ flex-wrap: wrap;
}
.users-list-header h3 {
margin: 0;
color: var(--dark-color);
+ flex-shrink: 0;
+}
+
+.users-header-actions {
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+ flex-wrap: wrap;
+ flex: 1;
+ justify-content: flex-end;
+}
+
+/* Users Search and Filter */
+.users-search-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 280px;
+}
+
+.users-search-input {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: var(--border-radius);
+ font-size: var(--font-size-sm);
+ width: 100%;
+ box-sizing: border-box;
+ transition: border-color 0.2s ease;
+}
+
+.users-search-input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(160, 44, 141, 0.1);
+}
+
+.users-filter-container {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+}
+
+.users-filter-select {
+ padding: 6px 10px;
+ border: 1px solid #ddd;
+ border-radius: var(--border-radius);
+ font-size: var(--font-size-xs);
+ background: white;
+ cursor: pointer;
+ flex: 1;
+}
+
+.users-filter-select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+#clear-users-search {
+ padding: 6px 8px;
+ font-size: 12px;
+ line-height: 1;
+ min-width: auto;
+}
+
+/* Search Results Info */
+.users-search-results {
+ background: #f8f9fa;
+ padding: 10px 15px;
+ border-radius: var(--border-radius);
+ margin-bottom: 15px;
+ border-left: 4px solid var(--primary-color);
+}
+
+.search-results-text {
+ margin: 0;
+ font-size: var(--font-size-sm);
+ color: #666;
+}
+
+/* No results state */
+.no-users-found {
+ text-align: center;
+ padding: 40px 20px;
+ color: #666;
+ font-style: italic;
+}
+
+/* Filtered state highlight */
+.users-cards-container.filtered {
+ border: 2px dashed #ddd;
+ padding: 10px;
+ border-radius: var(--border-radius);
+}
+
+.user-card.search-highlight {
+ box-shadow: 0 0 0 2px rgba(160, 44, 141, 0.2);
}
/* Helper Text */
diff --git a/map/app/public/js/admin-core.js b/map/app/public/js/admin-core.js
index 3794ee4..87aef43 100644
--- a/map/app/public/js/admin-core.js
+++ b/map/app/public/js/admin-core.js
@@ -262,8 +262,18 @@ function loadSectionData(sectionId) {
}
break;
case 'users':
- if (typeof loadUsers === 'function') {
- loadUsers();
+ // Load users and set up event listeners
+ if (window.adminUsers && typeof window.adminUsers.loadUsers === 'function') {
+ if (!window.adminUsers.isInitialized) {
+ console.log('Loading users for first time...');
+ window.adminUsers.loadUsers();
+ } else {
+ console.log('Users already initialized');
+ // Ensure event listeners are set up even if already initialized
+ if (typeof window.adminUsers.setupUserEventListeners === 'function') {
+ window.adminUsers.setupUserEventListeners();
+ }
+ }
}
break;
case 'start-location':
diff --git a/map/app/public/js/admin-users.js b/map/app/public/js/admin-users.js
index 0ac8fcb..8d9b571 100644
--- a/map/app/public/js/admin-users.js
+++ b/map/app/public/js/admin-users.js
@@ -5,6 +5,11 @@
// User management state
let allUsersData = [];
+let filteredUsersData = [];
+let currentSearchTerm = '';
+let currentFilterType = 'all';
+let eventListenersSetup = false;
+let usersInitialized = false;
// User Management Functions
async function loadUsers() {
@@ -23,7 +28,17 @@ async function loadUsers() {
if (loadingEl) loadingEl.style.display = 'none';
if (data.success && data.users) {
- displayUsers(data.users);
+ allUsersData = data.users;
+ filteredUsersData = [...allUsersData];
+ displayUsers(filteredUsersData);
+ updateSearchResults();
+
+ // Make sure event listeners are set up after loading users
+ if (!eventListenersSetup) {
+ setupUserEventListeners();
+ }
+
+ usersInitialized = true;
} else {
throw new Error(data.error || 'Failed to load users');
}
@@ -61,113 +76,171 @@ function displayUsers(users) {
}
if (!users || users.length === 0) {
- usersTableContainer.innerHTML = '
No users found.
';
+ const hasFilter = currentSearchTerm || currentFilterType !== 'all';
+ const emptyMessage = hasFilter ?
+ 'No users match your search criteria.' :
+ 'No users found.';
+ usersTableContainer.innerHTML = `
${emptyMessage}
`;
return;
}
- const tableHtml = `
-
-
-
-
- Email
- Name
- Phone
- Role
- Created
- Actions
-
-
-
- ${users.map(user => {
- const createdDate = user.created_at || user['Created At'] || user.createdAt;
- const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
- const isAdmin = user.admin || user.Admin || false;
- const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user');
- const userId = user.Id || user.id || user.ID;
-
- // Handle expiration info
- let expirationInfo = '';
- if (user.ExpiresAt) {
- const expirationDate = new Date(user.ExpiresAt);
- const now = new Date();
- const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));
-
- if (daysUntilExpiration < 0) {
- expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago `;
- } else if (daysUntilExpiration <= 3) {
- expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''} `;
- } else {
- expirationInfo = `Expires: ${expirationDate.toLocaleDateString()} `;
- }
- }
+ const cardsHtml = `
+
+ ${users.map(user => {
+ const createdDate = user.created_at || user['Created At'] || user.createdAt;
+ const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
+ const isAdmin = user.admin || user.Admin || false;
+ const userType = user.UserType || user.userType || user['User Type'] || (isAdmin ? 'admin' : 'user');
+ const userId = user.Id || user.id || user.ID;
+
+ // Handle expiration info
+ let expirationInfo = '';
+ if (user.ExpiresAt) {
+ const expirationDate = new Date(user.ExpiresAt);
+ const now = new Date();
+ const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));
+
+ if (daysUntilExpiration < 0) {
+ expirationInfo = `
Expired ${Math.abs(daysUntilExpiration)} days ago `;
+ } else if (daysUntilExpiration <= 3) {
+ expirationInfo = `
Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''} `;
+ } else {
+ expirationInfo = `
Expires: ${expirationDate.toLocaleDateString()} `;
+ }
+ }
- return `
-
- ${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')}
- ${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')}
- ${window.adminCore.escapeHtml(user.phone || user.Phone || 'N/A')}
-
-
- ${userType.charAt(0).toUpperCase() + userType.slice(1)}
-
- ${expirationInfo}
-
- ${formattedDate}
-
-
-
-
-
- Send Login Details
-
-
- Delete
-
-
-
-
-
- `;
- }).join('')}
-
-
+ const cardClass = user.ExpiresAt && new Date(user.ExpiresAt) < new Date() ? 'user-card expired' :
+ (user.ExpiresAt && new Date(user.ExpiresAt) - new Date() < 3 * 24 * 60 * 60 * 1000 ? 'user-card expires-soon' : 'user-card');
+
+ return `
+
+
+
+
+ 📞 ${window.adminCore.escapeHtml(user.phone || user.Phone || 'No phone')} • 📅 ${formattedDate}
+
+
+
+
+ `;
+ }).join('')}
Loading...
`;
- usersTableContainer.innerHTML = tableHtml;
+ usersTableContainer.innerHTML = cardsHtml;
setupUserActionListeners();
}
+// Keep for backward compatibility
+function searchUsers(searchTerm, filterType = 'all') {
+ // Update the form inputs and call filterUsers
+ const searchInput = document.getElementById('users-search');
+ const filterSelect = document.getElementById('users-filter-type');
+
+ if (searchInput) searchInput.value = searchTerm;
+ if (filterSelect) filterSelect.value = filterType;
+
+ filterUsers();
+}
+
+function updateSearchResults() {
+ const resultsElement = document.getElementById('users-search-results');
+ const resultsText = document.querySelector('.search-results-text');
+
+ if (!resultsElement || !resultsText) return;
+
+ // Get current values from form
+ const searchInput = document.getElementById('users-search');
+ const filterSelect = document.getElementById('users-filter-type');
+
+ const searchTerm = searchInput ? searchInput.value.trim() : '';
+ const filterType = filterSelect ? filterSelect.value : 'all';
+
+ const hasFilter = searchTerm || filterType !== 'all';
+
+ if (hasFilter) {
+ resultsElement.style.display = 'block';
+
+ let message = `Showing ${filteredUsersData.length} of ${allUsersData.length} users`;
+
+ if (searchTerm && filterType !== 'all') {
+ message += ` matching "${searchTerm}" in ${filterType} accounts`;
+ } else if (searchTerm) {
+ message += ` matching "${searchTerm}"`;
+ } else if (filterType !== 'all') {
+ message += ` with ${filterType} accounts`;
+ }
+
+ resultsText.textContent = message;
+ } else {
+ resultsElement.style.display = 'none';
+ }
+}
+
+function clearSearch() {
+ const searchInput = document.getElementById('users-search');
+ const filterSelect = document.getElementById('users-filter-type');
+
+ if (searchInput) searchInput.value = '';
+ if (filterSelect) filterSelect.value = 'all';
+
+ filterUsers();
+}
+
function setupUserActionListeners() {
const container = document.querySelector('.users-list');
if (!container) return;
- // Remove existing event listeners by cloning the container
- const newContainer = container.cloneNode(true);
- container.parentNode.replaceChild(newContainer, container);
+ // Use event delegation instead of replacing containers
+ // This avoids destroying search controls and their event listeners
- // Get the updated reference
- const updatedContainer = document.querySelector('.users-list');
-
- updatedContainer.addEventListener('click', function(e) {
+ // Remove any existing click listeners on the container to avoid duplicates
+ if (container._userActionHandler) {
+ container.removeEventListener('click', container._userActionHandler);
+ }
+
+ // Create the new handler
+ const newHandler = function(e) {
if (e.target.classList.contains('delete-user-btn')) {
const userId = e.target.getAttribute('data-user-id');
const userEmail = e.target.getAttribute('data-user-email');
@@ -182,7 +255,11 @@ function setupUserActionListeners() {
console.log('Email All Users button clicked');
showEmailUsersModal();
}
- });
+ };
+
+ // Add the new handler and store reference for removal later
+ container.addEventListener('click', newHandler);
+ container._userActionHandler = newHandler;
}
async function deleteUser(userId, userEmail) {
@@ -334,8 +411,72 @@ function clearUserForm() {
}
}
+// Simple search function like cuts
+function filterUsers() {
+ const searchTerm = document.getElementById('users-search').value.toLowerCase();
+ const filterType = document.getElementById('users-filter-type').value;
+
+ let filteredUsers = allUsersData;
+
+ // Apply type filter first
+ if (filterType !== 'all') {
+ filteredUsers = filteredUsers.filter(user => {
+ const userType = user.UserType || user.userType || user['User Type'] ||
+ (user.admin || user.Admin ? 'admin' : 'user');
+ return userType === filterType;
+ });
+ }
+
+ // Apply search filter
+ if (searchTerm) {
+ filteredUsers = filteredUsers.filter(user => {
+ // Handle different possible field names and null/undefined values
+ const userName = user.name || user.Name || '';
+ const userEmail = user.email || user.Email || '';
+ const userPhone = user.phone || user.Phone || '';
+
+ // Prioritize name matches - if name matches, return true immediately
+ if (userName.toLowerCase().includes(searchTerm)) {
+ return true;
+ }
+
+ // Then check email and phone
+ return userEmail.toLowerCase().includes(searchTerm) ||
+ userPhone.toLowerCase().includes(searchTerm);
+ });
+
+ // Sort results to prioritize name matches at the top
+ filteredUsers.sort((a, b) => {
+ const nameA = a.name || a.Name || '';
+ const nameB = b.name || b.Name || '';
+ const nameAMatches = nameA.toLowerCase().includes(searchTerm);
+ const nameBMatches = nameB.toLowerCase().includes(searchTerm);
+
+ // If both match by name or both don't match by name, maintain original order
+ if (nameAMatches === nameBMatches) {
+ return 0;
+ }
+
+ // Prioritize name matches (true comes before false)
+ return nameBMatches - nameAMatches;
+ });
+ }
+
+ filteredUsersData = filteredUsers;
+ displayUsers(filteredUsersData);
+ updateSearchResults();
+}
+
// Setup user-related event listeners
function setupUserEventListeners() {
+ // Prevent duplicate event listener setup
+ if (eventListenersSetup) {
+ console.log('User event listeners already set up, skipping');
+ return;
+ }
+
+ console.log('Setting up user event listeners...');
+
// User form submission
const userForm = document.getElementById('create-user-form');
if (userForm) {
@@ -373,6 +514,60 @@ function setupUserEventListeners() {
}
});
}
+
+ // Set up search and filter (simple like cuts manager)
+ const searchInput = document.getElementById('users-search');
+ if (searchInput) {
+ console.log('✅ Setting up users search input listener');
+ searchInput.addEventListener('input', () => filterUsers());
+ } else {
+ console.log('⚠️ users-search input not found');
+ }
+
+ const filterSelect = document.getElementById('users-filter-type');
+ if (filterSelect) {
+ console.log('✅ Setting up users filter select listener');
+ filterSelect.addEventListener('change', () => filterUsers());
+ } else {
+ console.log('⚠️ users-filter-type select not found');
+ }
+
+ // Clear search button
+ const clearSearchBtn = document.getElementById('clear-users-search');
+ if (clearSearchBtn) {
+ clearSearchBtn.addEventListener('click', clearSearch);
+ }
+
+ // Mark event listeners as set up
+ eventListenersSetup = true;
+}
+
+// Debug function to test search manually
+function debugUserSearch(testTerm = 'test') {
+ console.log('=== DEBUG USER SEARCH ===');
+ console.log('All users count:', allUsersData.length);
+ console.log('Current search term:', currentSearchTerm);
+ console.log('Current filter type:', currentFilterType);
+ console.log('Filtered users count:', filteredUsersData.length);
+
+ // Set test term in search input and trigger filter
+ const searchInput = document.getElementById('users-search');
+ const filterSelect = document.getElementById('users-filter-type');
+ if (searchInput) searchInput.value = testTerm;
+ if (filterSelect) filterSelect.value = 'all';
+
+ console.log('Testing search with term:', testTerm);
+ filterUsers();
+
+ console.log('After search - Filtered users count:', filteredUsersData.length);
+ console.log('========================');
+
+ return {
+ allUsers: allUsersData.length,
+ filteredUsers: filteredUsersData.length,
+ searchTerm: currentSearchTerm,
+ filterType: currentFilterType
+ };
}
// Export user management functions
@@ -384,6 +579,16 @@ window.adminUsers = {
createUser,
clearUserForm,
setupUserEventListeners,
+ searchUsers,
+ filterUsers,
+ clearSearch,
+ updateSearchResults,
+ debugUserSearch,
getAllUsersData: () => allUsersData,
- setAllUsersData: (data) => { allUsersData = data; }
+ getFilteredUsersData: () => filteredUsersData,
+ setAllUsersData: (data) => {
+ allUsersData = data;
+ filteredUsersData = [...allUsersData];
+ },
+ get isInitialized() { return usersInitialized; }
};