Updates to user management
This commit is contained in:
parent
006cbcf9c3
commit
b61e48f4fc
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -629,12 +629,36 @@
|
||||
<div class="users-list">
|
||||
<div class="users-list-header">
|
||||
<h3>All Users</h3>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="email-all-users-btn">
|
||||
📧 Email All Users
|
||||
<div class="users-header-actions">
|
||||
<div class="users-search-container">
|
||||
<input type="text"
|
||||
id="users-search"
|
||||
class="users-search-input"
|
||||
placeholder="Search users by name, email, or phone...">
|
||||
<div class="users-filter-container">
|
||||
<select id="users-filter-type" class="users-filter-select">
|
||||
<option value="all">All Users</option>
|
||||
<option value="admin">Admins</option>
|
||||
<option value="user">Regular Users</option>
|
||||
<option value="temp">Temporary Users</option>
|
||||
</select>
|
||||
<button id="clear-users-search" class="btn btn-secondary btn-sm">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="email-all-users-btn" class="btn btn-primary">
|
||||
<span>📧</span> Email All Users
|
||||
</button>
|
||||
</div>
|
||||
<!-- User table will be dynamically inserted here -->
|
||||
<p id="users-loading" class="loading-message">Loading users...</p>
|
||||
</div>
|
||||
<!-- User cards will be dynamically inserted here -->
|
||||
<div id="users-search-results" class="users-search-results" style="display: none;">
|
||||
<p class="search-results-text"></p>
|
||||
</div>
|
||||
<div class="users-table-container">
|
||||
<!-- Users will be inserted here -->
|
||||
</div>
|
||||
<div id="users-loading" class="loading-message" style="display: none;">Loading users...</div>
|
||||
<div id="users-empty" class="no-users-found" style="display: none;">No users found.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
.user-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.users-table tbody,
|
||||
.users-table tr,
|
||||
.users-table td {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
.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 tr {
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.users-table td {
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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':
|
||||
|
||||
@ -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,29 +76,21 @@ function displayUsers(users) {
|
||||
}
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
usersTableContainer.innerHTML = '<p class="empty-message">No users found.</p>';
|
||||
const hasFilter = currentSearchTerm || currentFilterType !== 'all';
|
||||
const emptyMessage = hasFilter ?
|
||||
'No users match your search criteria.' :
|
||||
'No users found.';
|
||||
usersTableContainer.innerHTML = `<div class="no-users-found">${emptyMessage}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const tableHtml = `
|
||||
<div class="users-table-wrapper">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Phone</th>
|
||||
<th>Role</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-table-body">
|
||||
const cardsHtml = `
|
||||
<div class="users-cards-container">
|
||||
${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 userType = user.UserType || user.userType || user['User Type'] || (isAdmin ? 'admin' : 'user');
|
||||
const userId = user.Id || user.id || user.ID;
|
||||
|
||||
// Handle expiration info
|
||||
@ -102,19 +109,29 @@ function displayUsers(users) {
|
||||
}
|
||||
}
|
||||
|
||||
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 `
|
||||
<tr ${user.ExpiresAt && new Date(user.ExpiresAt) < new Date() ? 'class="expired"' : (user.ExpiresAt && new Date(user.ExpiresAt) - new Date() < 3 * 24 * 60 * 60 * 1000 ? 'class="expires-soon"' : '')}>
|
||||
<td data-label="Email">${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')}</td>
|
||||
<td data-label="Name">${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')}</td>
|
||||
<td data-label="Phone">${window.adminCore.escapeHtml(user.phone || user.Phone || 'N/A')}</td>
|
||||
<td data-label="Role">
|
||||
<div class="${cardClass}">
|
||||
<div class="user-card-header">
|
||||
<div class="user-info-primary">
|
||||
<h4 class="user-name">${window.adminCore.escapeHtml(user.name || user.Name || 'N/A')}</h4>
|
||||
<p class="user-email">${window.adminCore.escapeHtml(user.email || user.Email || 'N/A')}</p>
|
||||
</div>
|
||||
<div class="user-role-badge">
|
||||
<span class="user-role ${userType}">
|
||||
${userType.charAt(0).toUpperCase() + userType.slice(1)}
|
||||
</span>
|
||||
${expirationInfo}
|
||||
</td>
|
||||
<td data-label="Created">${formattedDate}</td>
|
||||
<td data-label="Actions">
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-card-body">
|
||||
<div class="user-details-compact">
|
||||
📞 ${window.adminCore.escapeHtml(user.phone || user.Phone || 'No phone')} • 📅 ${formattedDate}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-card-footer">
|
||||
<div class="user-actions">
|
||||
<div class="user-communication-actions">
|
||||
<a href="mailto:${window.adminCore.escapeHtml(user.email || user.Email)}"
|
||||
@ -135,39 +152,95 @@ function displayUsers(users) {
|
||||
</div>
|
||||
<div class="user-admin-actions">
|
||||
<button class="btn btn-secondary btn-sm send-login-btn" data-user-id="${userId}" data-user-email="${window.adminCore.escapeHtml(user.email || user.Email)}">
|
||||
Send Login Details
|
||||
Send Login
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm delete-user-btn" data-user-id="${userId}" data-user-email="${window.adminCore.escapeHtml(user.email || user.Email)}">
|
||||
Delete
|
||||
<button class="btn btn-sm delete-user-btn" data-user-id="${userId}" data-user-email="${window.adminCore.escapeHtml(user.email || user.Email)}" title="Delete User">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="users-loading" class="loading-message" style="display: none;">Loading...</p>
|
||||
`;
|
||||
|
||||
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');
|
||||
// Remove any existing click listeners on the container to avoid duplicates
|
||||
if (container._userActionHandler) {
|
||||
container.removeEventListener('click', container._userActionHandler);
|
||||
}
|
||||
|
||||
updatedContainer.addEventListener('click', function(e) {
|
||||
// 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; }
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user