Sep 11 - Reupload the code
This commit is contained in:
@@ -0,0 +1,830 @@
|
||||
/* Admin Logs Styles - Enhanced version */
|
||||
.logs-page {
|
||||
padding: var(--spacing-6);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-8);
|
||||
padding-bottom: var(--spacing-6);
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: var(--spacing-4);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
/* Log Statistics */
|
||||
.log-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--white);
|
||||
padding: var(--spacing-6);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--white);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
);
|
||||
}
|
||||
|
||||
.stat-icon.security {
|
||||
background: linear-gradient(135deg, #f59e0b, #b45309);
|
||||
}
|
||||
|
||||
.stat-icon.errors {
|
||||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||||
}
|
||||
|
||||
.stat-icon.users {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.stat-info p {
|
||||
color: var(--gray-500);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Log Controls */
|
||||
.log-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-6);
|
||||
padding: var(--spacing-4);
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-10);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
position: absolute;
|
||||
left: var(--spacing-3);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--white);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Logs Table */
|
||||
.logs-table-container {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.logs-table th,
|
||||
.logs-table td {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
background: var(--gray-50);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
font-size: var(--font-size-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logs-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.logs-table tr.severity-high {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.logs-table tr.severity-medium {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.logs-table tr.severity-low {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.logs-table tr.severity-info {
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--gray-600);
|
||||
white-space: nowrap;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
font-size: var(--font-size-sm);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.category-badge,
|
||||
.severity-badge {
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.category-badge.security {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.category-badge.database {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.category-badge.user_activity {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.category-badge.system {
|
||||
background: #f3e8ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.severity-badge.high {
|
||||
background: #ef4444;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.severity-badge.medium {
|
||||
background: #f59e0b;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.severity-badge.low {
|
||||
background: #10b981;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.severity-badge.info {
|
||||
background: #3b82f6;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.description:hover {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.ip-address {
|
||||
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--gray-500);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Loading and Empty States */
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--spacing-12);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.loading-state i {
|
||||
font-size: var(--font-size-2xl);
|
||||
margin-bottom: var(--spacing-4);
|
||||
color: var(--primary-color);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: var(--spacing-4);
|
||||
color: var(--gray-300);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--gray-700);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-4);
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.pagination-controls button {
|
||||
min-width: 80px;
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border: 1px solid var(--gray-300);
|
||||
background: var(--white);
|
||||
color: var(--gray-700);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.pagination-controls button:hover:not(:disabled) {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.pagination-controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
/* Page number buttons */
|
||||
.page-number {
|
||||
min-width: 35px !important;
|
||||
margin: 0 2px;
|
||||
padding: var(--spacing-1) var(--spacing-2) !important;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: var(--white);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.page-number:hover:not(.active) {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.page-number.active {
|
||||
background: var(--primary-color) !important;
|
||||
color: var(--white) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.pagination-ellipsis {
|
||||
padding: var(--spacing-1) var(--spacing-2);
|
||||
color: var(--gray-500);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.pagination-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-xl);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-6);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
color: var(--gray-900);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--font-size-xl);
|
||||
cursor: pointer;
|
||||
color: var(--gray-400);
|
||||
padding: var(--spacing-1);
|
||||
border-radius: var(--radius);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--gray-600);
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--spacing-6);
|
||||
}
|
||||
|
||||
.modal-body .form-group {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-2);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.modal-body .form-control {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3);
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.modal-body .form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-6);
|
||||
border-top: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.warning-note {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: var(--radius);
|
||||
padding: var(--spacing-4);
|
||||
margin-top: var(--spacing-4);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.warning-note i {
|
||||
color: #f59e0b;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warning-note strong {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* Button Enhancements */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-500);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-600);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.logs-page {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.log-controls {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logs-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.logs-table th,
|
||||
.logs-table td {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: var(--spacing-4);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enhanced Animations */
|
||||
.stat-card {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.stat-card:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.stat-card:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.stat-card:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.stat-card:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.logs-table tbody tr {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus and accessibility improvements */
|
||||
.btn:focus,
|
||||
.filter-select:focus,
|
||||
.search-box input:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.logs-header .header-actions,
|
||||
.log-controls,
|
||||
.pagination-wrapper,
|
||||
.modal {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logs-page {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.logs-table th,
|
||||
.logs-table td {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal error states */
|
||||
.modal-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.modal-error i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Debug info for development */
|
||||
.debug-info {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
display: none; /* Show only in debug mode */
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* Attendance Fullscreen CSS
|
||||
* Optimized for iPad and tablet viewing
|
||||
* static/css/attendance_fullscreen.css
|
||||
*/
|
||||
|
||||
/* ===========================
|
||||
FULLSCREEN TOGGLE BUTTON
|
||||
=========================== */
|
||||
.fullscreen-toggle-btn {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fullscreen-toggle-btn:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8, #1e40af);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.fullscreen-toggle-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
FULLSCREEN MODE BASE
|
||||
=========================== */
|
||||
.fullscreen-mode {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
max-width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
background: #f8fafc !important;
|
||||
z-index: 9998 !important;
|
||||
overflow: auto !important;
|
||||
padding: 20px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
HIDE UI ELEMENTS IN FULLSCREEN
|
||||
=========================== */
|
||||
|
||||
/* Hide sidebar and overlays */
|
||||
.fullscreen-mode ~ #sidebar,
|
||||
.fullscreen-mode ~ .sidebar-overlay,
|
||||
body.fullscreen-active #sidebar,
|
||||
body.fullscreen-active .sidebar-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Adjust main wrapper */
|
||||
body.fullscreen-active .main-wrapper {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Hide header completely */
|
||||
.fullscreen-mode .attendance-header,
|
||||
.fullscreen-mode > .attendance-header {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide ALL stats sections with various possible class names */
|
||||
.fullscreen-mode .stats-section,
|
||||
.fullscreen-mode > .stats-section,
|
||||
.fullscreen-mode .stats-overview,
|
||||
.fullscreen-mode > .stats-overview,
|
||||
.fullscreen-mode .statistics-section,
|
||||
.fullscreen-mode > .statistics-section,
|
||||
.fullscreen-mode [class*="stat"][class*="section"],
|
||||
.fullscreen-mode div[class*="stat-"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Exception: Keep stat cards inside table section visible */
|
||||
.fullscreen-mode .attendance-table-section .stat-card,
|
||||
.fullscreen-mode .table-section .stat-card {
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
COMPACT FILTERS IN FULLSCREEN
|
||||
=========================== */
|
||||
.fullscreen-mode .filters-section {
|
||||
margin-bottom: 1rem !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filters-card {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filters-header {
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
background: #f8fafc !important;
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filters-header h3 {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filters-form {
|
||||
padding: 1rem 1.5rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-row {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(5, 1fr) !important;
|
||||
gap: 0.75rem !important;
|
||||
align-items: end !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-group {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 0.25rem !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-group label {
|
||||
font-size: 0.75rem !important;
|
||||
margin-bottom: 0.25rem !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
color: #374151 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-group input,
|
||||
.fullscreen-mode .filter-group select {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
height: 38px !important;
|
||||
border: 2px solid #e2e8f0 !important;
|
||||
border-radius: 6px !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-group input:focus,
|
||||
.fullscreen-mode .filter-group select:focus {
|
||||
border-color: #2563eb !important;
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-actions {
|
||||
grid-column: 1 / -1 !important;
|
||||
display: flex !important;
|
||||
gap: 0.75rem !important;
|
||||
justify-content: flex-start !important;
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-actions .btn {
|
||||
padding: 0.5rem 1rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
height: 38px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
TABLE SECTION IN FULLSCREEN
|
||||
=========================== */
|
||||
.fullscreen-mode .attendance-table-section {
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
background: white !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .table-header {
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
background: #f8fafc !important;
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .table-header h3 {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .table-controls {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .table-container {
|
||||
max-height: calc(100vh - 280px) !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table {
|
||||
font-size: 0.8125rem !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th,
|
||||
.fullscreen-mode .attendance-table td {
|
||||
padding: 0.625rem 0.5rem !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
background: #f8fafc !important;
|
||||
z-index: 10 !important;
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.025em !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
TABLE COLUMN OPTIMIZATION
|
||||
=========================== */
|
||||
|
||||
/* Prevent text overlap - allow wrapping for long content */
|
||||
.fullscreen-mode .attendance-table td {
|
||||
white-space: normal !important;
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
max-width: 200px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Keep headers on single line */
|
||||
.fullscreen-mode .attendance-table th {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
/* Specific column width optimizations */
|
||||
.fullscreen-mode .attendance-table th:nth-child(1),
|
||||
.fullscreen-mode .attendance-table td:nth-child(1) {
|
||||
/* # column */
|
||||
width: 40px !important;
|
||||
min-width: 40px !important;
|
||||
max-width: 40px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(2),
|
||||
.fullscreen-mode .attendance-table td:nth-child(2) {
|
||||
/* Employee ID */
|
||||
width: 80px !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 80px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(3),
|
||||
.fullscreen-mode .attendance-table td:nth-child(3) {
|
||||
/* Employee Name */
|
||||
width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
max-width: 120px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(4),
|
||||
.fullscreen-mode .attendance-table td:nth-child(4) {
|
||||
/* Location */
|
||||
width: 150px !important;
|
||||
min-width: 150px !important;
|
||||
max-width: 150px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(5),
|
||||
.fullscreen-mode .attendance-table td:nth-child(5) {
|
||||
/* Event */
|
||||
width: 80px !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 80px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(6),
|
||||
.fullscreen-mode .attendance-table td:nth-child(6) {
|
||||
/* Date */
|
||||
width: 100px !important;
|
||||
min-width: 100px !important;
|
||||
max-width: 100px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(7),
|
||||
.fullscreen-mode .attendance-table td:nth-child(7) {
|
||||
/* Time */
|
||||
width: 80px !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 80px !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(8),
|
||||
.fullscreen-mode .attendance-table td:nth-child(8) {
|
||||
/* QR Address */
|
||||
width: 180px !important;
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(9),
|
||||
.fullscreen-mode .attendance-table td:nth-child(9) {
|
||||
/* Check-in Address */
|
||||
width: 180px !important;
|
||||
min-width: 180px !important;
|
||||
max-width: 180px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(10),
|
||||
.fullscreen-mode .attendance-table td:nth-child(10) {
|
||||
/* Location Accuracy */
|
||||
width: 100px !important;
|
||||
min-width: 100px !important;
|
||||
max-width: 100px !important;
|
||||
white-space: nowrap !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(11),
|
||||
.fullscreen-mode .attendance-table td:nth-child(11) {
|
||||
/* Device */
|
||||
width: 100px !important;
|
||||
min-width: 100px !important;
|
||||
max-width: 100px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th:nth-child(12),
|
||||
.fullscreen-mode .attendance-table td:nth-child(12) {
|
||||
/* Actions */
|
||||
width: 80px !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 80px !important;
|
||||
white-space: nowrap !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* Address columns - allow text to wrap within container */
|
||||
.fullscreen-mode .address-column,
|
||||
.fullscreen-mode .attendance-table td:nth-child(8),
|
||||
.fullscreen-mode .attendance-table td:nth-child(9) {
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1.3 !important;
|
||||
padding: 0.5rem 0.375rem !important;
|
||||
}
|
||||
|
||||
/* Location accuracy badge styling */
|
||||
.fullscreen-mode .location-accuracy-badge,
|
||||
.fullscreen-mode .accuracy-badge {
|
||||
display: inline-block !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 0.7rem !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
/* Device info */
|
||||
.fullscreen-mode .device-info {
|
||||
font-size: 0.75rem !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Employee info styling */
|
||||
.fullscreen-mode .employee-id,
|
||||
.fullscreen-mode .employee-name {
|
||||
font-size: 0.8125rem !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
/* Location and event info */
|
||||
.fullscreen-mode .location-info,
|
||||
.fullscreen-mode .event-info {
|
||||
font-size: 0.75rem !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.25rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .location-info i,
|
||||
.fullscreen-mode .event-info i {
|
||||
flex-shrink: 0 !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Address info containers */
|
||||
.fullscreen-mode .address-info {
|
||||
display: flex !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 0.25rem !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .address-info i {
|
||||
flex-shrink: 0 !important;
|
||||
margin-top: 0.125rem !important;
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Time display */
|
||||
.fullscreen-mode .time-info {
|
||||
font-size: 0.75rem !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
background: #f1f5f9 !important;
|
||||
border-radius: 4px !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.fullscreen-mode .record-actions {
|
||||
display: flex !important;
|
||||
gap: 0.25rem !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .action-btn {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
font-size: 0.7rem !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
/* Table layout */
|
||||
.fullscreen-mode .attendance-table {
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Ensure table container allows horizontal scroll if needed */
|
||||
.fullscreen-mode .table-container {
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
RESPONSIVE BREAKPOINTS
|
||||
=========================== */
|
||||
|
||||
/* iPad Landscape */
|
||||
@media (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) {
|
||||
.fullscreen-mode {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-row {
|
||||
grid-template-columns: repeat(3, 1fr) !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-actions {
|
||||
grid-column: 1 / -1 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table {
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table th,
|
||||
.fullscreen-mode .attendance-table td {
|
||||
padding: 0.5rem 0.375rem !important;
|
||||
}
|
||||
|
||||
/* Adjust column widths for iPad landscape */
|
||||
.fullscreen-mode .attendance-table th:nth-child(8),
|
||||
.fullscreen-mode .attendance-table td:nth-child(8),
|
||||
.fullscreen-mode .attendance-table th:nth-child(9),
|
||||
.fullscreen-mode .attendance-table td:nth-child(9) {
|
||||
width: 150px !important;
|
||||
min-width: 150px !important;
|
||||
max-width: 150px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPad Portrait */
|
||||
@media (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) {
|
||||
.fullscreen-mode {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-row {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-actions {
|
||||
grid-column: 1 / -1 !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .attendance-table {
|
||||
font-size: 0.7rem !important;
|
||||
}
|
||||
|
||||
/* Hide less important columns on iPad portrait */
|
||||
.fullscreen-mode .attendance-table th:nth-child(11),
|
||||
.fullscreen-mode .attendance-table td:nth-child(11) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Adjust remaining column widths */
|
||||
.fullscreen-mode .attendance-table th:nth-child(8),
|
||||
.fullscreen-mode .attendance-table td:nth-child(8),
|
||||
.fullscreen-mode .attendance-table th:nth-child(9),
|
||||
.fullscreen-mode .attendance-table td:nth-child(9) {
|
||||
width: 140px !important;
|
||||
min-width: 140px !important;
|
||||
max-width: 140px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 767px) {
|
||||
.fullscreen-toggle-btn {
|
||||
width: 45px !important;
|
||||
height: 45px !important;
|
||||
top: 15px !important;
|
||||
right: 15px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode .filter-row {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
/* Show only essential columns on mobile */
|
||||
.fullscreen-mode .attendance-table th:nth-child(8),
|
||||
.fullscreen-mode .attendance-table td:nth-child(8),
|
||||
.fullscreen-mode .attendance-table th:nth-child(9),
|
||||
.fullscreen-mode .attendance-table td:nth-child(9),
|
||||
.fullscreen-mode .attendance-table th:nth-child(10),
|
||||
.fullscreen-mode .attendance-table td:nth-child(10),
|
||||
.fullscreen-mode .attendance-table th:nth-child(11),
|
||||
.fullscreen-mode .attendance-table td:nth-child(11) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
SCROLLBAR STYLING
|
||||
=========================== */
|
||||
.fullscreen-mode::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.fullscreen-mode::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.fullscreen-mode::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.fullscreen-mode::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
OVERSCROLL PREVENTION
|
||||
=========================== */
|
||||
.fullscreen-mode {
|
||||
overscroll-behavior: none !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
body.fullscreen-active {
|
||||
overscroll-behavior: none !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
PRINT STYLES
|
||||
=========================== */
|
||||
@media print {
|
||||
.fullscreen-toggle-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fullscreen-mode {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Authentication page specific styles
|
||||
* static/css/auth.css
|
||||
*
|
||||
* This file contains all authentication-related styles for login/register pages
|
||||
*/
|
||||
|
||||
/* Authentication Container - Center the form */
|
||||
.auth-container {
|
||||
min-height: 100vh !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
padding: 2rem 1rem !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Authentication Card - Modern centered design */
|
||||
.auth-card {
|
||||
background: var(--white) !important;
|
||||
border-radius: var(--radius-2xl) !important;
|
||||
box-shadow: var(--shadow-xl) !important;
|
||||
padding: 3rem !important;
|
||||
width: 100% !important;
|
||||
max-width: 420px !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
height: 4px !important;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
}
|
||||
|
||||
/* Authentication Header */
|
||||
.auth-header {
|
||||
text-align: center !important;
|
||||
margin-bottom: 2.5rem !important;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
border-radius: 50% !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.auth-logo i {
|
||||
font-size: 2.5rem !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 2.25rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: var(--gray-900) !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
.auth-header p {
|
||||
color: var(--gray-500) !important;
|
||||
font-size: 1.125rem !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* Authentication Form */
|
||||
.auth-form {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group {
|
||||
margin-bottom: 2rem !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
color: var(--gray-700) !important;
|
||||
margin-bottom: 0.75rem !important;
|
||||
font-size: 0.925rem !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group label i {
|
||||
width: 20px !important;
|
||||
text-align: center !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group input {
|
||||
width: 100% !important;
|
||||
padding: 1rem !important;
|
||||
border: 2px solid var(--gray-200) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
font-size: 1rem !important;
|
||||
transition: var(--transition) !important;
|
||||
background-color: var(--white) !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group input:focus {
|
||||
outline: none !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group.focused label {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.auth-form .btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
gap: 0.75rem !important;
|
||||
padding: 1rem 2rem !important;
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
border: none !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
cursor: pointer !important;
|
||||
transition: var(--transition) !important;
|
||||
text-decoration: none !important;
|
||||
min-height: 52px !important;
|
||||
}
|
||||
|
||||
.auth-form .btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
color: var(--white) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
.auth-form .btn-primary:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
.auth-form .btn-primary:active {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.auth-form .btn-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.auth-form .btn:disabled {
|
||||
opacity: 0.7 !important;
|
||||
cursor: not-allowed !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Remove Auth Footer (Register Link) */
|
||||
.auth-footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.auth-container {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 2rem !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
width: 70px !important;
|
||||
height: 70px !important;
|
||||
}
|
||||
|
||||
.auth-logo i {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-card {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.auth-header h1 {
|
||||
font-size: 1.75rem !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.fa-spinner {
|
||||
animation: spin 1s linear infinite !important;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus Accessibility */
|
||||
.auth-form .btn:focus,
|
||||
.auth-form .form-group input:focus {
|
||||
outline: 2px solid var(--primary-color) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
/* High Contrast Mode Support */
|
||||
@media (prefers-contrast: high) {
|
||||
.auth-card {
|
||||
border: 2px solid var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.auth-form .form-group input {
|
||||
border-width: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion Support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.auth-form .btn-primary:hover,
|
||||
.auth-form .form-group input:focus {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Turnstile Integration */
|
||||
.turnstile-container {
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
margin: 1.5rem 0 !important;
|
||||
}
|
||||
|
||||
.turnstile-container .cf-turnstile {
|
||||
transform: scale(0.95) !important;
|
||||
transform-origin: center !important;
|
||||
}
|
||||
|
||||
/* Responsive Turnstile */
|
||||
@media screen and (max-width: 480px) {
|
||||
.turnstile-container .cf-turnstile {
|
||||
transform: scale(0.85) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form validation enhancement for Turnstile */
|
||||
.auth-form .form-group.turnstile-error {
|
||||
border: 2px solid var(--error-color) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
padding: 1rem !important;
|
||||
background-color: rgba(239, 68, 68, 0.05) !important;
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
.turnstile-container .cf-turnstile[data-theme="dark"] {
|
||||
background-color: var(--gray-800) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,823 @@
|
||||
/**
|
||||
* Employee Management Page Styles
|
||||
* static/css/employees.css
|
||||
*/
|
||||
|
||||
/* Main Container */
|
||||
.employees-page {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Sidebar Layout Compatibility */
|
||||
body.has-sidebar .employees-page {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.main-wrapper .employees-page {
|
||||
padding: 2rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.employees-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.employees-header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.header-navigation {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid #cbd5e1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
transform: translateX(-2px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.header-description {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Statistics Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card.search-results {
|
||||
border: 2px solid #fbbf24;
|
||||
background: #fefbf2;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
color: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-card.search-results .stat-icon {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-info p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Search Section */
|
||||
.search-section {
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
font-size: 1rem;
|
||||
background: #ffffff;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.clear-search-btn {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
|
||||
.search-input-group:has(.clear-search-btn) .search-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.search-btn:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.clear-search-btn:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
/* Employee Table Container */
|
||||
.employees-table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table-header h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-info {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.employees-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.employees-table thead {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.employees-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.employees-table tbody tr {
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.employees-table tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.employees-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Table Cell Specific Styles */
|
||||
.row-number {
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.employee-id .id-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.employee-name {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
color: #ffffff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.name-details h4 {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.name-details p {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.employee-title .title-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #ecfdf5;
|
||||
color: #065f46;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.employee-title .no-title {
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.contract .contract-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.actions {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #0891b2;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #0e7490;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination-container {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pagination-nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pagination-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.pagination-link:hover {
|
||||
background: #f3f4f6;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
.pagination-link.current {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
color: #d1d5db;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #dc2626;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-header h3 i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: #374151;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
margin-bottom: 1rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.modal-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.375rem;
|
||||
color: #b91c1c;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.warning-text i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Search Highlighting */
|
||||
mark {
|
||||
background: #fef08a;
|
||||
color: #854d0e;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.employees-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.employees-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.employees-table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.employees-table th,
|
||||
.employees-table td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
|
||||
.name-container {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.modal.show .modal-content {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #dc2626;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: #374151;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #dc2626;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Enhanced button styles for better interaction feedback */
|
||||
.btn-danger:hover {
|
||||
background-color: #b91c1c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background-color: #b91c1c !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
/* Loading state for delete button */
|
||||
.btn-danger:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -0,0 +1,897 @@
|
||||
/* Export Configuration Styles */
|
||||
.export-config-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.export-header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem 2rem;
|
||||
margin: 0 auto 2rem;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.header-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.header-title-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
color: #2d3748;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
color: #48bb78;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
color: #718096;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Applied Filters Section */
|
||||
.applied-filters-section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filters-header h3 {
|
||||
color: #2d3748;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filters-header i {
|
||||
color: #4299e1;
|
||||
}
|
||||
|
||||
.filters-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
background: linear-gradient(135deg, #4299e1, #3182ce);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 25px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(66, 153, 225, 0.3);
|
||||
}
|
||||
|
||||
/* Export Configuration Section */
|
||||
.export-config-section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.config-header h3 {
|
||||
color: #2d3748;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.config-header i {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Columns Grid */
|
||||
.columns-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
background: linear-gradient(135deg, #f7fafc, #edf2f7);
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-item:hover {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 4px 16px rgba(66, 153, 225, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.column-checkbox {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.column-checkbox input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-checkbox label i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #cbd5e0;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
color: transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-checkbox input[type="checkbox"]:checked + label {
|
||||
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.column-checkbox input[type="checkbox"]:checked + label i {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.column-name-input {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-name-input label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.column-name-input input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.column-name-input input:focus {
|
||||
outline: none;
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
|
||||
}
|
||||
|
||||
/* Preview Section */
|
||||
.preview-section {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-header h3 {
|
||||
color: #2d3748;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.preview-header i {
|
||||
color: #ed8936;
|
||||
}
|
||||
|
||||
.preview-table-container {
|
||||
overflow-x: auto;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.preview-table th {
|
||||
background: linear-gradient(135deg, #4299e1, #3182ce);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #2b6cb0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preview-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #a0aec0;
|
||||
font-style: italic;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Submit Section */
|
||||
.submit-section {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.submit-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.submit-note {
|
||||
color: #718096;
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submit-note i {
|
||||
color: #4299e1;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.65rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #48bb78, #38a169);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: linear-gradient(135deg, #38a169, #2f855a);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #a0aec0, #718096);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #718096, #4a5568);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #4299e1;
|
||||
border: 2px solid #4299e1;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.export-config-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.export-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-title-section {
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.columns-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.config-card,
|
||||
.preview-section,
|
||||
.submit-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.filters-display {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Instructions Card */
|
||||
.instructions-card {
|
||||
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
|
||||
border: 2px solid #4fc3f7;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.15);
|
||||
}
|
||||
|
||||
.instructions-content h4 {
|
||||
color: #1565c0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.instructions-content ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.instructions-content li {
|
||||
padding: 0.5rem 0;
|
||||
color: #424242;
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.instructions-content li::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.instructions-content .highlight {
|
||||
background: linear-gradient(135deg, #ffeb3b, #ffc107);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
/* Columns Section */
|
||||
.columns-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.columns-section h4 {
|
||||
color: #2d3748;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Selected Columns Section */
|
||||
.selected-columns-section {
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
border: 2px dashed #6c757d;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-top: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.selected-columns-section.has-columns {
|
||||
border-color: #28a745;
|
||||
border-style: solid;
|
||||
background: linear-gradient(135deg, #f8fff8, #e8f5e8);
|
||||
}
|
||||
|
||||
.selected-columns-section h4 {
|
||||
color: #495057;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Selected Columns List */
|
||||
.selected-columns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.selected-column-item {
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: move;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.selected-column-item:hover {
|
||||
border-color: #4299e1;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(66, 153, 225, 0.15);
|
||||
}
|
||||
|
||||
.selected-column-item.sortable-drag {
|
||||
opacity: 0.8;
|
||||
transform: rotate(5deg);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.selected-column-item.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.selected-column-item.sortable-chosen {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.selected-column-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.column-drag-handle {
|
||||
color: #9ca3af;
|
||||
font-size: 1.2rem;
|
||||
cursor: grab;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.column-drag-handle:hover {
|
||||
color: #4b5563;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.column-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.selected-column-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selected-column-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.selected-column-export-name {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.column-order-number {
|
||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
/* Empty state for selected columns */
|
||||
.selected-columns-empty {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.selected-columns-empty i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Enhanced column items in grid */
|
||||
.column-item.selected {
|
||||
border-color: #10b981;
|
||||
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.column-item.selected .column-checkbox label {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive design for drag & drop */
|
||||
@media (max-width: 768px) {
|
||||
.selected-columns-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.selected-column-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.selected-column-info {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.column-order-number {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation for smooth transitions */
|
||||
.selected-columns-list .selected-column-item {
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Sortable.js additional styles */
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Enhanced preview table to show column order */
|
||||
.preview-table thead th {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-table thead th::before {
|
||||
content: attr(data-order);
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #4f46e5;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Collapsible Sections */
|
||||
.collapsible-content {
|
||||
max-height: 5000px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.5s ease-in-out, opacity 0.3s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.collapsible-content.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.3s ease;
|
||||
margin-left: 10px;
|
||||
font-size: 0.9em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.toggle-icon.rotated {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.config-header[onclick],
|
||||
.selected-columns-section h4[onclick] {
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: -10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.config-header[onclick]:hover,
|
||||
.selected-columns-section h4[onclick]:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.config-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.selected-columns-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Edit checkbox styles */
|
||||
.export-name-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-checkbox input[type="text"] {
|
||||
flex: 1;
|
||||
background-color: #f8f9fa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input-with-checkbox input[type="text"]:not([readonly]) {
|
||||
background-color: white;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.edit-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.edit-checkbox-label:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.edit-checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-checkbox-label i {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.edit-checkbox-label input[type="checkbox"]:checked + i {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.column-name-input input[type="text"][readonly] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* Projects Page Dedicated CSS
|
||||
* static/css/projects.css
|
||||
*
|
||||
* This file contains all necessary styles for the projects management page
|
||||
* ensuring it works independently with proper layout and responsive design.
|
||||
*/
|
||||
|
||||
/* Sidebar Layout Compatibility Fixes */
|
||||
body.has-sidebar .projects-page {
|
||||
margin-left: 0; /* Let the base template handle sidebar spacing */
|
||||
padding-left: 0; /* Remove conflicts with sidebar layout */
|
||||
}
|
||||
|
||||
/* Ensure proper content area for authenticated layout */
|
||||
.main-wrapper .projects-page {
|
||||
padding: 2rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Fix header spacing in sidebar layout */
|
||||
body.has-sidebar .projects-header {
|
||||
margin-top: 0; /* Remove any unwanted top margin */
|
||||
}
|
||||
|
||||
/* Projects Page Container */
|
||||
.projects-page {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.projects-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projects-header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.header-navigation {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid #cbd5e1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
transform: translateX(-2px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.back-button i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Project Statistics Grid */
|
||||
.project-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.stat-card.active::before {
|
||||
background: linear-gradient(90deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.stat-icon.active {
|
||||
background: linear-gradient(135deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-info p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.content-section {
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Projects Grid */
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8);
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.project-card.inactive {
|
||||
opacity: 0.7;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.project-card.inactive::before {
|
||||
background: linear-gradient(90deg, #6b7280, #4b5563);
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.project-info h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.project-description {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #047857;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
color: #4b5563;
|
||||
border: 1px solid rgba(107, 114, 128, 0.2);
|
||||
}
|
||||
|
||||
/* Project Stats */
|
||||
.project-stats-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.stat-item i {
|
||||
width: 16px;
|
||||
color: #2563eb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item span {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* Project Actions */
|
||||
.project-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.project-actions .btn {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.project-actions .btn-secondary {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.project-actions .btn-secondary:hover {
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.project-actions .btn-warning {
|
||||
background: #fbbf24;
|
||||
color: #92400e;
|
||||
border: 1px solid #f59e0b;
|
||||
}
|
||||
|
||||
.project-actions .btn-warning:hover {
|
||||
background: #f59e0b;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
.project-actions .btn-success {
|
||||
background: #10b981;
|
||||
color: #ffffff;
|
||||
border: 1px solid #059669;
|
||||
}
|
||||
|
||||
.project-actions .btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1.5rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
background: #f1f5f9;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.empty-state .btn {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.empty-state .btn:hover {
|
||||
background: #1d4ed8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-navigation {
|
||||
text-align: left;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.project-status {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.project-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.project-actions .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.projects-page {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.project-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.project-stats-section {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,974 @@
|
||||
/* QR Destination Page Styles - Light Mode Only */
|
||||
:root {
|
||||
/* Color Palette - Light Mode Only */
|
||||
--primary-color: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--success-color: #059669;
|
||||
--warning-color: #d97706;
|
||||
--danger-color: #dc2626;
|
||||
--info-color: #0891b2;
|
||||
|
||||
/* Neutral Colors - Light Theme Fixed */
|
||||
--white: #ffffff;
|
||||
--gray-50: #f8fafc;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
|
||||
/* Spacing and Layout */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius: 0.375rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
--transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* Force Light Mode - Override any dark mode preferences */
|
||||
* {
|
||||
color-scheme: light only !important;
|
||||
}
|
||||
|
||||
/* Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
min-height: 100vh;
|
||||
color: var(--gray-900) !important;
|
||||
line-height: 1.6;
|
||||
/* Force light mode */
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.destination-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.destination-header {
|
||||
text-align: center;
|
||||
color: var(--white) !important;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
border-radius: 50%;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.destination-header h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
opacity: 0.9;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
/* Card Styles - Force Light Theme */
|
||||
.info-card,
|
||||
.checkin-card,
|
||||
.success-card {
|
||||
background: var(--white) !important;
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-xl);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
color: var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Header Styles for Cards */
|
||||
.info-header,
|
||||
.checkin-header,
|
||||
.success-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-header h2,
|
||||
.checkin-header h2,
|
||||
.success-header h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900) !important;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-header h2 i,
|
||||
.checkin-header h2 i,
|
||||
.success-header h2 i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.info-header p,
|
||||
.checkin-header p {
|
||||
color: var(--gray-600) !important;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Info Details */
|
||||
.info-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--gray-50) !important;
|
||||
border-radius: var(--radius-lg);
|
||||
transition: var(--transition);
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
background: var(--gray-100) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--primary-color) !important;
|
||||
color: var(--white) !important;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900) !important;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--gray-700) !important;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Form Styles - Light Theme */
|
||||
.checkin-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group label i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 1rem 1.25rem;
|
||||
border: 2px solid var(--gray-300) !important;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 1.125rem;
|
||||
transition: var(--transition);
|
||||
background: var(--white) !important;
|
||||
color: var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: var(--gray-500) !important;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-600) !important;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Button Styles - Light Theme */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
cursor: pointer;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-500) !important;
|
||||
color: var(--white) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-600) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-loader {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-loader i {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Success Card Styles - Light Theme */
|
||||
.success-card {
|
||||
text-align: center;
|
||||
background: var(--white) !important;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
display: none; /* Change back to 'inline-flex' to display icon */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: var(--success-color) !important;
|
||||
color: var(--white) !important;
|
||||
border-radius: 50%;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
animation: successPulse 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.success-card h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--success-color) !important;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.success-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.success-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--gray-50) !important;
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--success-color) !important;
|
||||
}
|
||||
|
||||
.success-item strong {
|
||||
color: var(--gray-900) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-item span {
|
||||
color: var(--gray-700) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.success-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Status Message Styles - Light Theme */
|
||||
.status-message {
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: 500;
|
||||
margin: 1rem 0;
|
||||
border-left: 4px solid transparent;
|
||||
background: var(--white) !important;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background: var(--gray-50) !important;
|
||||
color: var(--success-color) !important;
|
||||
border-left-color: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background: var(--gray-50) !important;
|
||||
color: var(--danger-color) !important;
|
||||
border-left-color: var(--danger-color) !important;
|
||||
}
|
||||
|
||||
.status-message.warning {
|
||||
background: var(--gray-50) !important;
|
||||
color: var(--warning-color) !important;
|
||||
border-left-color: var(--warning-color) !important;
|
||||
}
|
||||
|
||||
.status-message.info {
|
||||
background: var(--gray-50) !important;
|
||||
color: var(--info-color) !important;
|
||||
border-left-color: var(--info-color) !important;
|
||||
}
|
||||
|
||||
/* Footer Styles - Light Theme */
|
||||
.destination-footer {
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
font-size: 0.875rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.destination-footer p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
.destination-footer i {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.destination-footer small {
|
||||
opacity: 0.7;
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Loading Overlay - Light Theme */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.loading-spinner p {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
color: var(--gray-700) !important;
|
||||
}
|
||||
|
||||
/* Language Selector Styles - Light Theme Enhanced */
|
||||
.language-selector {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.language-toggle {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3) !important;
|
||||
border-radius: 50px;
|
||||
padding: 0.5rem 1rem;
|
||||
color: white !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.language-toggle:active {
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.language-toggle:focus {
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.language-toggle i {
|
||||
font-size: 1rem;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Smooth transitions - SIMPLIFIED */
|
||||
.fade-transition {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-transition.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Additional Location Tracking Styles - Light Theme */
|
||||
.location-status {
|
||||
background: var(--gray-50) !important;
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin: 15px 0;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--gray-700) !important;
|
||||
}
|
||||
|
||||
.location-status.loading {
|
||||
background: #e0f2fe !important;
|
||||
border-color: #0284c7 !important;
|
||||
color: #0c4a6e !important;
|
||||
}
|
||||
|
||||
.location-status.success {
|
||||
background: #dcfce7 !important;
|
||||
border-color: #059669 !important;
|
||||
color: #14532d !important;
|
||||
}
|
||||
|
||||
.location-status.error {
|
||||
background: #fee2e2 !important;
|
||||
border-color: #dc2626 !important;
|
||||
color: #7f1d1d !important;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
background: var(--gray-50) !important;
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
color: var(--gray-700) !important;
|
||||
}
|
||||
|
||||
.location-info h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.coord-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--gray-200) !important;
|
||||
color: var(--gray-700) !important;
|
||||
}
|
||||
|
||||
.coord-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.coord-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
color: var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.location-controls {
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
background: var(--gray-500) !important;
|
||||
color: white !important;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin: 0 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background: var(--gray-600) !important;
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.language-selector {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.destination-container {
|
||||
padding: 1rem 0.5rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.destination-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.checkin-card,
|
||||
.success-card {
|
||||
margin: 0 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.success-details {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-item {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.destination-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.destination-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
font-size: 1rem;
|
||||
padding: 0.875rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility - High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.info-card,
|
||||
.checkin-card,
|
||||
.success-card {
|
||||
border: 2px solid var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
border: 1px solid var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
border: 2px solid var(--gray-900) !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gray-900) !important;
|
||||
border: 2px solid var(--white) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.detail-item:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.destination-container {
|
||||
max-width: none;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.checkin-card,
|
||||
.success-card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #000 !important;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.loading-overlay,
|
||||
.language-selector {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.destination-header {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
background: white !important;
|
||||
border: 2px solid black !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Override any dark mode preferences - IMPORTANT */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Keep light theme variables even when system prefers dark */
|
||||
--white: #ffffff !important;
|
||||
--gray-50: #f8fafc !important;
|
||||
--gray-100: #f1f5f9 !important;
|
||||
--gray-200: #e2e8f0 !important;
|
||||
--gray-300: #cbd5e1 !important;
|
||||
--gray-400: #94a3b8 !important;
|
||||
--gray-500: #64748b !important;
|
||||
--gray-600: #475569 !important;
|
||||
--gray-700: #334155 !important;
|
||||
--gray-800: #1e293b !important;
|
||||
--gray-900: #0f172a !important;
|
||||
}
|
||||
|
||||
/* Force light mode styles */
|
||||
body {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)) !important;
|
||||
color: var(--gray-900) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Force override any dark theme attributes */
|
||||
[data-theme="dark"] {
|
||||
/* Reset to light theme */
|
||||
--white: #ffffff !important;
|
||||
--gray-50: #f8fafc !important;
|
||||
--gray-100: #f1f5f9 !important;
|
||||
--gray-200: #e2e8f0 !important;
|
||||
--gray-300: #cbd5e1 !important;
|
||||
--gray-400: #94a3b8 !important;
|
||||
--gray-500: #64748b !important;
|
||||
--gray-600: #475569 !important;
|
||||
--gray-700: #334155 !important;
|
||||
--gray-800: #1e293b !important;
|
||||
--gray-900: #0f172a !important;
|
||||
}
|
||||
|
||||
/* Location Services Warning Banner */
|
||||
.location-warning-banner {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(238, 90, 36, 0.3);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.warning-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.warning-retry-btn,
|
||||
.warning-dismiss-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.warning-retry-btn:hover,
|
||||
.warning-dismiss-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Blocked Form Styling */
|
||||
.location-blocked {
|
||||
opacity: 0.6;
|
||||
pointer-events: none !important;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.location-blocked::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Specifically block submit buttons when location is blocked */
|
||||
button[data-location-blocked="true"],
|
||||
input[data-location-blocked="true"] {
|
||||
pointer-events: none !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
background-color: #ccc !important;
|
||||
border-color: #999 !important;
|
||||
}
|
||||
|
||||
/* Override any hover effects on blocked elements */
|
||||
button[data-location-blocked="true"]:hover,
|
||||
button[data-location-blocked="true"]:focus,
|
||||
button[data-location-blocked="true"]:active {
|
||||
pointer-events: none !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
transform: none !important;
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
/* Animation for warning banner */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.warning-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.warning-retry-btn,
|
||||
.warning-dismiss-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
/* Statistics Page Styles - static/css/statistics.css */
|
||||
|
||||
:root {
|
||||
--primary-color: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--success-color: #10b981;
|
||||
--warning-color: #f59e0b;
|
||||
--danger-color: #ef4444;
|
||||
--info-color: #06b6d4;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
--white: #ffffff;
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.2s ease-in-out;
|
||||
--radius: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--spacing-2: 0.5rem;
|
||||
--spacing-3: 0.75rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-8: 2rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 2rem;
|
||||
}
|
||||
|
||||
/* Collapsible Sections */
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--white);
|
||||
padding: var(--spacing-4) var(--spacing-6);
|
||||
border-radius: var(--radius-xl);
|
||||
margin-bottom: var(--spacing-6);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: linear-gradient(135deg, var(--primary-hover), #1d4ed8);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transition: transform 0.3s ease;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.toggle-icon.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-content.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Chart No Data Message */
|
||||
.no-data-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--gray-400);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-data-message i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: var(--spacing-4);
|
||||
color: var(--gray-300);
|
||||
}
|
||||
|
||||
.no-data-message p {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* Collapsible Table Headers */
|
||||
.table-header.collapsible {
|
||||
background: linear-gradient(135deg, var(--gray-100), var(--gray-200));
|
||||
color: var(--gray-700);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
user-select: none;
|
||||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||
}
|
||||
|
||||
.table-header.collapsible:hover {
|
||||
background: linear-gradient(135deg, var(--gray-200), var(--gray-300));
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.table-header.collapsible h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-header.collapsible .toggle-icon {
|
||||
margin-left: auto;
|
||||
margin-right: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* Update table card structure for collapsible */
|
||||
.table-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
/* Ensure section content collapses properly */
|
||||
.section-content.collapsed {
|
||||
max-height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Fix the chevron animation */
|
||||
.toggle-icon {
|
||||
transition: transform 0.3s ease !important;
|
||||
font-size: var(--font-size-base);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toggle-icon.rotated {
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
.statistics-page {
|
||||
padding: var(--spacing-6);
|
||||
background: var(--gray-50);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.statistics-header {
|
||||
background: linear-gradient(135deg, var(--white) 0%, var(--gray-50) 100%);
|
||||
border-radius: var(--radius-2xl);
|
||||
padding: var(--spacing-8);
|
||||
margin-bottom: var(--spacing-8);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: var(--spacing-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
color: var(--primary-color);
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-lg);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Filters Section */
|
||||
.filters-section {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.filters-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--white);
|
||||
padding: var(--spacing-6);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.filters-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
padding: var(--spacing-6);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: var(--spacing-6);
|
||||
margin-bottom: var(--spacing-6);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.filter-group label i {
|
||||
color: var(--primary-color);
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
padding: var(--spacing-3);
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-base);
|
||||
transition: var(--transition);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.filter-group input:focus,
|
||||
.filter-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Statistics Overview */
|
||||
.stats-overview {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--spacing-6);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
transition: var(--transition);
|
||||
border: 1px solid var(--gray-200);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.stat-card.primary::before { background: var(--primary-color); }
|
||||
.stat-card.success::before { background: var(--success-color); }
|
||||
.stat-card.warning::before { background: var(--warning-color); }
|
||||
.stat-card.info::before { background: var(--info-color); }
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: var(--white);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-card.primary .stat-icon { background: var(--primary-color); }
|
||||
.stat-card.success .stat-icon { background: var(--success-color); }
|
||||
.stat-card.warning .stat-icon { background: var(--warning-color); }
|
||||
.stat-card.info .stat-icon { background: var(--info-color); }
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-content h3 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
margin: 0 0 var(--spacing-2) 0;
|
||||
}
|
||||
|
||||
.stat-content p {
|
||||
color: var(--gray-600);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
margin: 0 0 var(--spacing-2) 0;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* Charts Section */
|
||||
.charts-section {
|
||||
margin-bottom: var(--spacing-8);
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.chart-card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
background: var(--gray-50);
|
||||
padding: var(--spacing-6);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.chart-header h3 i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: var(--spacing-6);
|
||||
height: 350px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-container.compact {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
/* Tables Section */
|
||||
.tables-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: var(--white);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background: var(--gray-50);
|
||||
padding: var(--spacing-6);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.table-header h3 i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.table-count {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-700);
|
||||
padding: var(--spacing-4);
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
/* Metric Badges */
|
||||
.metric-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.metric-badge.primary { background: var(--primary-color); }
|
||||
.metric-badge.success { background: var(--success-color); }
|
||||
.metric-badge.info { background: var(--info-color); }
|
||||
.metric-badge.warning { background: var(--warning-color); }
|
||||
|
||||
/* Progress Bars */
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
background: var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--success-color), #059669);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Special Elements */
|
||||
.ip-address {
|
||||
background: var(--gray-100);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.user-info strong {
|
||||
display: block;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.user-info small {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.activity-score .score {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.activity-score .score.high {
|
||||
background: var(--success-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.activity-score .score.medium {
|
||||
background: var(--warning-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.activity-score .score.low {
|
||||
background: var(--gray-400);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.performance-indicator .perf {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.performance-indicator .perf.excellent {
|
||||
background: var(--success-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.performance-indicator .perf.good {
|
||||
background: var(--info-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.performance-indicator .perf.fair {
|
||||
background: var(--warning-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.performance-indicator .perf.poor {
|
||||
background: var(--danger-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.performance-indicator small {
|
||||
display: block;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--spacing-3) var(--spacing-6);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-600);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-700);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--gray-600);
|
||||
border: 2px solid var(--gray-300);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--gray-400);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.statistics-page {
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.statistics-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--spacing-2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header-content h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.statistics-page {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-actions,
|
||||
.filters-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chart-card,
|
||||
.table-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* theme-gov.css
|
||||
* =============
|
||||
* UI theme overrides for GOV Services, Inc. (qr.govservicesinc.com)
|
||||
*
|
||||
* HOW IT WORKS
|
||||
* ------------
|
||||
* Loaded AFTER style.css but BEFORE page-specific CSS (e.g. auth.css).
|
||||
* Overrides CSS custom properties in :root so every component that uses
|
||||
* those variables (sidebar, buttons, login page, links, focus rings)
|
||||
* adopts the new palette automatically.
|
||||
*
|
||||
* Brand text is handled directly in templates via {{ COMPANY_NAME }} —
|
||||
* no CSS tricks needed.
|
||||
*
|
||||
* TO CUSTOMISE COLORS
|
||||
* -------------------
|
||||
* Change --primary-color / --primary-hover below.
|
||||
* All gradients, buttons, focus rings, sidebar, and login page pick
|
||||
* up the new values automatically via CSS variable inheritance.
|
||||
*
|
||||
* ADDING A THIRD THEME (future)
|
||||
* -----------------------------
|
||||
* Create static/css/theme-{name}.css and set THEME_NAME={name} in
|
||||
* that instance's .env. No code changes required.
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 1. COLOR PALETTE — GOV Services green */
|
||||
/* ------------------------------------------------------------------ */
|
||||
:root {
|
||||
/* Primary brand color: bright institutional green */
|
||||
--primary-color: #16a34a;
|
||||
--primary-hover: #15803d;
|
||||
|
||||
/* Accent used for links, focus rings, active states */
|
||||
--info-color: #16a34a;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 2. SIDEBAR GRADIENT */
|
||||
/* Re-declared to consume the new variables above. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
.sidebar {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 3. LOGIN PAGE — auth.css loads after this file (via extra_head) so */
|
||||
/* we must use !important to win the cascade on rules that auth.css */
|
||||
/* also declares with !important. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Login background gradient */
|
||||
.auth-container {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Logo circle gradient */
|
||||
.auth-logo {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Card top accent stripe */
|
||||
.auth-card::before {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Sign In button gradient */
|
||||
.auth-form .btn-primary {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--primary-color),
|
||||
var(--primary-hover)
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Input focus border colour */
|
||||
.auth-form .form-group input:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
/* Override the hardcoded blue rgba(37,99,235) in auth.css */
|
||||
box-shadow: 0 0 0 4px rgba(22, 163, 74, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Focused label colour */
|
||||
.auth-form .form-group.focused label {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* Label icon colour */
|
||||
.auth-form .form-group label i {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* Focus accessibility outline */
|
||||
.auth-form .btn:focus,
|
||||
.auth-form .form-group input:focus {
|
||||
outline-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 4. FOOTER COPYRIGHT */
|
||||
/* Handled dynamically in templates via {{ CURRENT_YEAR }} and */
|
||||
/* {{ COMPANY_NAME }} — no CSS override needed here. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,723 @@
|
||||
/**
|
||||
* Users Page Dedicated CSS
|
||||
* static/css/users-page.css
|
||||
*
|
||||
* This file contains all necessary styles for the users management page
|
||||
* ensuring it works independently of other CSS files.
|
||||
*/
|
||||
|
||||
/* Users Page Container */
|
||||
.users-page {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.users-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.users-header::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #059669, #047857);
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.header-content h1 i {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
color: #64748b;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* User Statistics Grid */
|
||||
.user-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid #e2e8f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.stat-card.admin::before {
|
||||
background: linear-gradient(90deg, #fbbf24, #f59e0b);
|
||||
}
|
||||
|
||||
.stat-card.staff::before {
|
||||
background: linear-gradient(90deg, #64748b, #475569);
|
||||
}
|
||||
|
||||
.stat-card.payroll::before {
|
||||
background: linear-gradient(90deg, #17a2b8, #138496);
|
||||
}
|
||||
|
||||
.stat-card.accounting::before {
|
||||
background: linear-gradient(90deg, #059669, #047857);
|
||||
}
|
||||
|
||||
.stat-card.project-manager::before {
|
||||
background: linear-gradient(90deg, #6f42c1, #5a2d91);
|
||||
}
|
||||
|
||||
.stat-card.active::before {
|
||||
background: linear-gradient(90deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.stat-card.inactive::before {
|
||||
background: linear-gradient(90deg, #ef4444, #b91c1c);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon.admin {
|
||||
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||
}
|
||||
|
||||
.stat-icon.staff {
|
||||
background: linear-gradient(135deg, #64748b, #475569);
|
||||
}
|
||||
|
||||
.stat-icon.payroll {
|
||||
background: linear-gradient(135deg, #17a2b8, #138496);
|
||||
}
|
||||
|
||||
.stat-icon.accounting {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
}
|
||||
|
||||
.stat-icon.project-manager {
|
||||
background: linear-gradient(135deg, #6f42c1, #5a2d91);
|
||||
}
|
||||
|
||||
.stat-icon.active {
|
||||
background: linear-gradient(135deg, #10b981, #047857);
|
||||
}
|
||||
|
||||
.stat-icon.inactive {
|
||||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stat-info p {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Users Controls */
|
||||
.users-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
color: #94a3b8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
width: 300px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.view-options {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Users Table Container */
|
||||
.users-table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
background-color: #f8fafc;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.users-table th:first-child {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.users-table td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.users-table tr.user-row {
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.users-table tr.user-row:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.users-table tr.user-row:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* User Information */
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-details h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.user-details p {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Contact Information */
|
||||
.user-contact {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.contact-email {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.contact-email i {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Role Badges */
|
||||
.user-role {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.user-role.admin {
|
||||
background-color: rgba(251, 191, 36, 0.1);
|
||||
color: #d97706;
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.user-role.staff {
|
||||
background-color: rgba(100, 116, 139, 0.1);
|
||||
color: #64748b;
|
||||
border: 1px solid rgba(100, 116, 139, 0.3);
|
||||
}
|
||||
|
||||
.user-role.payroll {
|
||||
background-color: rgba(23, 162, 184, 0.1);
|
||||
color: #138496;
|
||||
border: 1px solid rgba(23, 162, 184, 0.3);
|
||||
}
|
||||
|
||||
.user-role.accounting {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: #047857;
|
||||
border: 1px solid rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
.user-role.project_manager {
|
||||
background-color: rgba(111, 66, 193, 0.1);
|
||||
color: #5a2d91;
|
||||
border: 1px solid rgba(111, 66, 193, 0.3);
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.user-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-status.active {
|
||||
background-color: rgba(5, 150, 105, 0.1);
|
||||
color: #10b981;
|
||||
border: 1px solid rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
|
||||
.user-status.inactive {
|
||||
background-color: rgba(220, 38, 38, 0.1);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
/* QR Code Count */
|
||||
.user-qr-count {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qr-count {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.qr-stats small {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Date Information */
|
||||
.date-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.date-main {
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.date-time {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.never-logged-in {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.user-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2563eb;
|
||||
color: #ffffff;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #64748b;
|
||||
color: #ffffff;
|
||||
border-color: #64748b;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #475569;
|
||||
border-color: #475569;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #10b981;
|
||||
color: #ffffff;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #047857;
|
||||
border-color: #047857;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #f59e0b;
|
||||
color: #ffffff;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #d97706;
|
||||
border-color: #d97706;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
color: #ffffff;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn:disabled,
|
||||
.btn[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:disabled:hover,
|
||||
.btn[disabled]:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* No Users Message */
|
||||
.no-users-message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.no-users-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.no-users-content i {
|
||||
font-size: 3rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.no-users-content h3 {
|
||||
font-size: 1.25rem;
|
||||
color: #334155;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.no-users-content p {
|
||||
color: #64748b;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.users-page {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.users-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.users-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Hide less important columns on mobile */
|
||||
.users-table th:nth-child(6),
|
||||
.users-table td:nth-child(6),
|
||||
.users-table th:nth-child(7),
|
||||
.users-table td:nth-child(7),
|
||||
.users-table th:nth-child(8),
|
||||
.users-table td:nth-child(8) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.user-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.users-table th,
|
||||
.users-table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.user-details h4 {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.user-details p {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
/* Hide more columns on very small screens */
|
||||
.users-table th:nth-child(5),
|
||||
.users-table td:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Android Location Fix - GPS + IP Geolocation Only
|
||||
* File: static/js/android_location_handler.js
|
||||
*
|
||||
* This version includes only precise location methods:
|
||||
* 1. Progressive GPS fallback (4 attempts)
|
||||
* 2. IP-based geolocation (3 services) - 5-50km accuracy
|
||||
* No timezone analysis (removed 100km accuracy method)
|
||||
*/
|
||||
|
||||
// Android-specific geolocation configuration
|
||||
const ANDROID_LOCATION_CONFIG = {
|
||||
// Primary attempt - High accuracy with reasonable timeout
|
||||
highAccuracy: {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 15000,
|
||||
maximumAge: 60000
|
||||
},
|
||||
|
||||
// Fallback attempt - Network-based location
|
||||
networkBased: {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 20000,
|
||||
maximumAge: 300000
|
||||
},
|
||||
|
||||
// Final attempt - Any available location
|
||||
anyLocation: {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 30000,
|
||||
maximumAge: 600000
|
||||
}
|
||||
};
|
||||
|
||||
// Global variables for location state
|
||||
let locationAttemptInProgress = false;
|
||||
let currentLocationMethod = '';
|
||||
|
||||
// Enhanced location request with GPS + IP fallback only
|
||||
function requestAndroidEnhancedLocation() {
|
||||
console.log("📱 Starting Android location request (GPS + IP methods only)...");
|
||||
|
||||
if (locationAttemptInProgress) {
|
||||
console.log("📍 Location attempt already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof locationRequestActive !== 'undefined' && locationRequestActive) {
|
||||
console.log("📍 Location request already active, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
console.log("❌ Geolocation not supported, trying IP-based location");
|
||||
attemptIPBasedLocation();
|
||||
return;
|
||||
}
|
||||
|
||||
locationAttemptInProgress = true;
|
||||
|
||||
// Set active flags
|
||||
if (typeof locationRequestActive !== 'undefined') {
|
||||
locationRequestActive = true;
|
||||
}
|
||||
if (typeof locationCaptureActive !== 'undefined') {
|
||||
locationCaptureActive = true;
|
||||
}
|
||||
|
||||
console.log("📱 Attempting GPS-based location sequence...");
|
||||
attemptAndroidLocationSequence();
|
||||
}
|
||||
|
||||
// GPS-based sequence (Steps 1-4)
|
||||
function attemptAndroidLocationSequence() {
|
||||
console.log("🔄 Step 1/5: High Accuracy GPS");
|
||||
currentLocationMethod = 'gps_high_accuracy';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
console.log("✅ GPS high-accuracy success!");
|
||||
handleAndroidLocationSuccess(position, "gps_high_accuracy");
|
||||
},
|
||||
(error) => {
|
||||
console.log(`❌ High accuracy failed (${error.message}), trying network-based...`);
|
||||
attemptNetworkBasedLocation();
|
||||
},
|
||||
ANDROID_LOCATION_CONFIG.highAccuracy
|
||||
);
|
||||
}
|
||||
|
||||
function attemptNetworkBasedLocation() {
|
||||
console.log("🔄 Step 2/5: Network-based GPS");
|
||||
currentLocationMethod = 'network';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
console.log("✅ Network-based GPS success!");
|
||||
handleAndroidLocationSuccess(position, "network");
|
||||
},
|
||||
(error) => {
|
||||
console.log(`❌ Network-based failed (${error.message}), trying any location...`);
|
||||
attemptAnyAvailableLocation();
|
||||
},
|
||||
ANDROID_LOCATION_CONFIG.networkBased
|
||||
);
|
||||
}
|
||||
|
||||
function attemptAnyAvailableLocation() {
|
||||
console.log("🔄 Step 3/5: Any available GPS");
|
||||
currentLocationMethod = 'any';
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
console.log("✅ Any-location GPS success!");
|
||||
handleAndroidLocationSuccess(position, "any");
|
||||
},
|
||||
(error) => {
|
||||
console.log(`❌ Any location failed (${error.message}), trying watchPosition...`);
|
||||
attemptWatchPosition();
|
||||
},
|
||||
ANDROID_LOCATION_CONFIG.anyLocation
|
||||
);
|
||||
}
|
||||
|
||||
function attemptWatchPosition() {
|
||||
console.log("🔄 Step 4/5: Watch Position (persistent)");
|
||||
currentLocationMethod = 'watch';
|
||||
|
||||
let watchId = null;
|
||||
let watchTimeout = null;
|
||||
|
||||
watchTimeout = setTimeout(() => {
|
||||
if (watchId !== null) {
|
||||
navigator.geolocation.clearWatch(watchId);
|
||||
}
|
||||
console.log("❌ Watch position timed out, trying IP-based location...");
|
||||
attemptIPBasedLocation();
|
||||
}, 25000);
|
||||
|
||||
watchId = navigator.geolocation.watchPosition(
|
||||
(position) => {
|
||||
console.log("✅ Watch position success!");
|
||||
navigator.geolocation.clearWatch(watchId);
|
||||
clearTimeout(watchTimeout);
|
||||
handleAndroidLocationSuccess(position, "watch");
|
||||
},
|
||||
(error) => {
|
||||
console.log(`❌ Watch position error: ${error.message}`);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: false,
|
||||
timeout: 20000,
|
||||
maximumAge: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// IP-based geolocation (Step 5) - Final fallback
|
||||
function attemptIPBasedLocation() {
|
||||
console.log("🔄 Step 5/5: IP-based Geolocation (Final Fallback)");
|
||||
currentLocationMethod = 'ip_geolocation';
|
||||
|
||||
// Try multiple IP geolocation services for better accuracy
|
||||
const ipLocationServices = [
|
||||
{
|
||||
url: 'https://ipinfo.io/json',
|
||||
parseResponse: (data) => {
|
||||
if (data.loc) {
|
||||
const [lat, lng] = data.loc.split(',');
|
||||
return {
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng),
|
||||
city: data.city,
|
||||
region: data.region,
|
||||
country: data.country,
|
||||
accuracy: data.city ? 15000 : 50000 // Better accuracy if city is available
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
{
|
||||
url: 'https://ipapi.co/json/',
|
||||
parseResponse: (data) => ({
|
||||
lat: data.latitude,
|
||||
lng: data.longitude,
|
||||
city: data.city,
|
||||
region: data.region,
|
||||
country: data.country_name,
|
||||
accuracy: data.city ? 10000 : 50000 // Better accuracy if city is available
|
||||
})
|
||||
},
|
||||
];
|
||||
|
||||
let serviceIndex = 0;
|
||||
|
||||
function tryNextIPService() {
|
||||
if (serviceIndex >= ipLocationServices.length) {
|
||||
console.log("❌ All IP geolocation services failed - location detection complete");
|
||||
handleAndroidLocationError("All GPS and IP geolocation methods failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const service = ipLocationServices[serviceIndex];
|
||||
console.log(`🌐 Trying IP geolocation service ${serviceIndex + 1}: ${service.url}`);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
fetch(service.url, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log(`🌐 IP service ${serviceIndex + 1} response:`, data);
|
||||
|
||||
const parsed = service.parseResponse(data);
|
||||
|
||||
if (parsed && parsed.lat && parsed.lng && !isNaN(parsed.lat) && !isNaN(parsed.lng)) {
|
||||
// Validate coordinates are reasonable
|
||||
if (parsed.lat >= -90 && parsed.lat <= 90 && parsed.lng >= -180 && parsed.lng <= 180) {
|
||||
console.log(`✅ IP-based location success: ${parsed.lat}, ${parsed.lng}`);
|
||||
console.log(`📊 Location: ${parsed.city}, ${parsed.region}, ${parsed.country}`);
|
||||
console.log(`📊 Estimated accuracy: ${parsed.accuracy}m (~${Math.round(parsed.accuracy/1000)}km)`);
|
||||
|
||||
const ipLocationData = {
|
||||
coords: {
|
||||
latitude: parsed.lat,
|
||||
longitude: parsed.lng,
|
||||
accuracy: parsed.accuracy,
|
||||
altitude: null
|
||||
},
|
||||
locationInfo: {
|
||||
city: parsed.city,
|
||||
region: parsed.region,
|
||||
country: parsed.country,
|
||||
source: `IP Service ${serviceIndex + 1}`,
|
||||
serviceUrl: service.url
|
||||
}
|
||||
};
|
||||
|
||||
handleAndroidLocationSuccess(ipLocationData, "ip_geolocation");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`❌ Invalid or missing coordinates from service ${serviceIndex + 1}, trying next...`);
|
||||
serviceIndex++;
|
||||
tryNextIPService();
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
console.log(`❌ IP service ${serviceIndex + 1} timed out (10s), trying next...`);
|
||||
} else {
|
||||
console.log(`❌ IP service ${serviceIndex + 1} failed: ${error.message}, trying next...`);
|
||||
}
|
||||
serviceIndex++;
|
||||
tryNextIPService();
|
||||
});
|
||||
}
|
||||
|
||||
tryNextIPService();
|
||||
}
|
||||
|
||||
// Enhanced success handler for GPS and IP location methods
|
||||
function handleAndroidLocationSuccess(position, source) {
|
||||
console.log(`✅ Android location obtained successfully via ${source}`);
|
||||
|
||||
let locationData;
|
||||
|
||||
if (source === "ip_geolocation") {
|
||||
// Handle IP-based location
|
||||
locationData = {
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy,
|
||||
altitude: position.coords.altitude,
|
||||
timestamp: new Date(),
|
||||
source: source,
|
||||
address: null,
|
||||
locationInfo: position.locationInfo || null
|
||||
};
|
||||
|
||||
console.log(`📍 IP-estimated coordinates: ${position.coords.latitude}, ${position.coords.longitude}`);
|
||||
console.log(`📊 IP-estimated accuracy: ${position.coords.accuracy}m (~${Math.round(position.coords.accuracy/1000)}km)`);
|
||||
if (position.locationInfo) {
|
||||
console.log(`🏢 Location info: ${position.locationInfo.city}, ${position.locationInfo.region}`);
|
||||
}
|
||||
} else {
|
||||
// Handle GPS-based location
|
||||
locationData = {
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
accuracy: position.coords.accuracy,
|
||||
altitude: position.coords.altitude,
|
||||
timestamp: new Date(),
|
||||
source: source,
|
||||
address: null,
|
||||
};
|
||||
|
||||
console.log(`📍 GPS coordinates: ${position.coords.latitude}, ${position.coords.longitude}`);
|
||||
console.log(`📊 GPS accuracy: ${position.coords.accuracy}m`);
|
||||
}
|
||||
|
||||
// Update global location variables
|
||||
if (typeof userLocation !== 'undefined') {
|
||||
Object.assign(userLocation, locationData);
|
||||
console.log("📍 Updated userLocation with location data");
|
||||
|
||||
// Trigger reverse geocoding if we have coordinates but no address
|
||||
if (typeof reverseGeocode === 'function' && locationData.latitude && locationData.longitude && !locationData.address) {
|
||||
reverseGeocode(locationData.latitude, locationData.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof currentUserLocation !== 'undefined') {
|
||||
Object.assign(currentUserLocation, locationData);
|
||||
console.log("📍 Updated currentUserLocation with location data");
|
||||
|
||||
// Trigger enhanced reverse geocoding if available
|
||||
if (typeof reverseGeocodeEnhanced === 'function' && locationData.latitude && locationData.longitude && !locationData.address) {
|
||||
reverseGeocodeEnhanced(locationData.latitude, locationData.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear active flags
|
||||
if (typeof locationRequestActive !== 'undefined') {
|
||||
locationRequestActive = false;
|
||||
}
|
||||
if (typeof locationCaptureActive !== 'undefined') {
|
||||
locationCaptureActive = false;
|
||||
}
|
||||
locationAttemptInProgress = false;
|
||||
|
||||
// Console logging
|
||||
console.log(`📊 LOCATION SUCCESS LOG:`, {
|
||||
method: source,
|
||||
success: true,
|
||||
coordinates: `${locationData.latitude},${locationData.longitude}`,
|
||||
accuracy: `${locationData.accuracy}m`,
|
||||
accuracyKm: `~${Math.round(locationData.accuracy/1000)}km`,
|
||||
locationInfo: locationData.locationInfo,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
function handleAndroidLocationError(errorMessage) {
|
||||
console.log(`❌ All Android location methods failed: ${errorMessage}`);
|
||||
|
||||
// Update global location variables to indicate manual source
|
||||
if (typeof userLocation !== 'undefined') {
|
||||
userLocation.source = "manual";
|
||||
}
|
||||
if (typeof currentUserLocation !== 'undefined') {
|
||||
currentUserLocation.source = "manual";
|
||||
}
|
||||
|
||||
// Clear active flags
|
||||
if (typeof locationRequestActive !== 'undefined') {
|
||||
locationRequestActive = false;
|
||||
}
|
||||
if (typeof locationCaptureActive !== 'undefined') {
|
||||
locationCaptureActive = false;
|
||||
}
|
||||
locationAttemptInProgress = false;
|
||||
|
||||
console.log(`📊 LOCATION ERROR LOG:`, {
|
||||
error: errorMessage,
|
||||
method: currentLocationMethod,
|
||||
finalResult: 'manual_entry_required',
|
||||
gpsAttempts: 4,
|
||||
ipAttempts: 3,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Device detection functions
|
||||
function isAndroidDevice() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('android');
|
||||
}
|
||||
|
||||
function isAndroidChrome() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('android') && userAgent.includes('chrome') && !userAgent.includes('edg');
|
||||
}
|
||||
|
||||
// Main initialization function
|
||||
function initializeAndroidLocation() {
|
||||
console.log("📱 Initializing Android location services (GPS + IP only)...");
|
||||
|
||||
if (isAndroidDevice()) {
|
||||
console.log("📱 Android device detected, using GPS + IP location methods (5 steps)");
|
||||
requestAndroidEnhancedLocation();
|
||||
} else {
|
||||
console.log("📱 Non-Android device, using standard location request");
|
||||
|
||||
if (typeof requestLocationData === 'function') {
|
||||
requestLocationData();
|
||||
} else if (typeof requestEnhancedLocation === 'function') {
|
||||
requestEnhancedLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override standard location initialization for Android devices
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(() => {
|
||||
if (isAndroidDevice()) {
|
||||
console.log("📱 Android detected - overriding with GPS + IP location handler");
|
||||
|
||||
if (typeof initializeLocation === 'function') {
|
||||
window.initializeLocation = function() {
|
||||
console.log("📱 Using GPS + IP Android location initialization");
|
||||
initializeAndroidLocation();
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof requestEnhancedLocation === 'function') {
|
||||
window.requestEnhancedLocation = function() {
|
||||
console.log("📱 Using GPS + IP Android location request");
|
||||
initializeAndroidLocation();
|
||||
};
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Export functions
|
||||
if (typeof window !== 'undefined') {
|
||||
window.AndroidLocationHandler = {
|
||||
requestAndroidEnhancedLocation,
|
||||
isAndroidDevice,
|
||||
isAndroidChrome,
|
||||
initializeAndroidLocation,
|
||||
attemptIPBasedLocation
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Attendance Fullscreen JavaScript
|
||||
* Handles fullscreen toggle and optimization for iPad viewing
|
||||
* static/js/attendance_fullscreen.js
|
||||
*/
|
||||
|
||||
// Fullscreen state management
|
||||
let isFullscreen = false;
|
||||
|
||||
// Touch event handlers
|
||||
let touchStartY = 0;
|
||||
let touchStartX = 0;
|
||||
|
||||
/**
|
||||
* Toggle fullscreen mode for attendance report
|
||||
*/
|
||||
function toggleFullscreen() {
|
||||
const container = document.getElementById('attendanceReportContainer');
|
||||
const icon = document.getElementById('fullscreenIcon');
|
||||
const body = document.body;
|
||||
|
||||
if (!container || !icon) {
|
||||
console.error('Fullscreen elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
isFullscreen = !isFullscreen;
|
||||
|
||||
if (isFullscreen) {
|
||||
enterFullscreen(container, icon, body);
|
||||
} else {
|
||||
exitFullscreen(container, icon, body);
|
||||
}
|
||||
|
||||
// Log fullscreen action
|
||||
logFullscreenAction(isFullscreen ? 'enter' : 'exit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent swipe gestures from interfering with fullscreen on touch devices
|
||||
*/
|
||||
function preventSwipeGestures(container, enable) {
|
||||
if (enable) {
|
||||
// Prevent pull-to-refresh and other touch gestures
|
||||
container.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
container.addEventListener('touchend', handleTouchEnd, { passive: false });
|
||||
|
||||
// Prevent overscroll
|
||||
document.body.style.overscrollBehavior = 'none';
|
||||
container.style.overscrollBehavior = 'none';
|
||||
|
||||
console.log('Swipe gestures prevented for fullscreen mode');
|
||||
} else {
|
||||
// Re-enable normal touch behavior
|
||||
container.removeEventListener('touchstart', handleTouchStart);
|
||||
container.removeEventListener('touchmove', handleTouchMove);
|
||||
container.removeEventListener('touchend', handleTouchEnd);
|
||||
|
||||
// Restore overscroll
|
||||
document.body.style.overscrollBehavior = '';
|
||||
container.style.overscrollBehavior = '';
|
||||
|
||||
console.log('Swipe gestures re-enabled');
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(e) {
|
||||
touchStartY = e.touches[0].clientY;
|
||||
touchStartX = e.touches[0].clientX;
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
if (!isFullscreen) return;
|
||||
|
||||
const touchY = e.touches[0].clientY;
|
||||
const touchX = e.touches[0].clientX;
|
||||
const deltaY = touchY - touchStartY;
|
||||
const deltaX = touchX - touchStartX;
|
||||
|
||||
const container = document.getElementById('attendanceReportContainer');
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
||||
|
||||
// Prevent pull-down-to-refresh when at top
|
||||
if (isAtTop && deltaY > 0) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent overscroll at bottom
|
||||
if (isAtBottom && deltaY < 0) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow normal scrolling within the container
|
||||
}
|
||||
|
||||
function handleTouchEnd(e) {
|
||||
touchStartY = 0;
|
||||
touchStartX = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter fullscreen mode
|
||||
*/
|
||||
function enterFullscreen(container, icon, body) {
|
||||
// Add fullscreen classes
|
||||
container.classList.add('fullscreen-mode');
|
||||
body.classList.add('fullscreen-active');
|
||||
|
||||
// Change icon
|
||||
icon.classList.remove('fa-expand');
|
||||
icon.classList.add('fa-compress');
|
||||
|
||||
// Update button title
|
||||
const button = document.getElementById('fullscreenToggle');
|
||||
if (button) {
|
||||
button.setAttribute('title', 'Exit Fullscreen');
|
||||
}
|
||||
|
||||
// Detect if device is touch-enabled (iPad/tablet)
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// Only use native fullscreen API on non-touch devices
|
||||
// This prevents swipe-down gesture from exiting fullscreen on iPad
|
||||
if (!isTouchDevice) {
|
||||
// Try to use native fullscreen API for desktop browsers
|
||||
if (container.requestFullscreen) {
|
||||
container.requestFullscreen().catch(err => {
|
||||
console.log('Native fullscreen not available, using CSS fullscreen');
|
||||
});
|
||||
} else if (container.webkitRequestFullscreen) {
|
||||
container.webkitRequestFullscreen().catch(err => {
|
||||
console.log('Native fullscreen not available, using CSS fullscreen');
|
||||
});
|
||||
} else if (container.mozRequestFullScreen) {
|
||||
container.mozRequestFullScreen().catch(err => {
|
||||
console.log('Native fullscreen not available, using CSS fullscreen');
|
||||
});
|
||||
} else if (container.msRequestFullscreen) {
|
||||
container.msRequestFullscreen().catch(err => {
|
||||
console.log('Native fullscreen not available, using CSS fullscreen');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Touch device detected - using CSS-only fullscreen to prevent swipe-down exit');
|
||||
}
|
||||
|
||||
// Adjust table layout for better viewing
|
||||
adjustTableForFullscreen(true);
|
||||
|
||||
// Save fullscreen preference
|
||||
saveFullscreenPreference(true);
|
||||
|
||||
// Prevent default touch behaviors that might interfere
|
||||
preventSwipeGestures(container, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit fullscreen mode
|
||||
*/
|
||||
function exitFullscreen(container, icon, body) {
|
||||
// Remove fullscreen classes
|
||||
container.classList.remove('fullscreen-mode');
|
||||
body.classList.remove('fullscreen-active');
|
||||
|
||||
// Change icon back
|
||||
icon.classList.remove('fa-compress');
|
||||
icon.classList.add('fa-expand');
|
||||
|
||||
// Update button title
|
||||
const button = document.getElementById('fullscreenToggle');
|
||||
if (button) {
|
||||
button.setAttribute('title', 'Toggle Fullscreen');
|
||||
}
|
||||
|
||||
// Exit native fullscreen if active (only for desktop)
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
if (!isTouchDevice) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.log('Native fullscreen exit not needed');
|
||||
});
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen().catch(err => {
|
||||
console.log('Native fullscreen exit not needed');
|
||||
});
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen().catch(err => {
|
||||
console.log('Native fullscreen exit not needed');
|
||||
});
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen().catch(err => {
|
||||
console.log('Native fullscreen exit not needed');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Restore table layout
|
||||
adjustTableForFullscreen(false);
|
||||
|
||||
// Save fullscreen preference
|
||||
saveFullscreenPreference(false);
|
||||
|
||||
// Re-enable default touch behaviors
|
||||
preventSwipeGestures(container, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust table columns visibility based on fullscreen and device
|
||||
*/
|
||||
function adjustTableForFullscreen(isFullscreen) {
|
||||
const table = document.getElementById('attendanceTable');
|
||||
if (!table) return;
|
||||
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'
|
||||
};
|
||||
|
||||
// Log viewport info for debugging
|
||||
console.log('Adjusting table for fullscreen:', {
|
||||
isFullscreen,
|
||||
viewport
|
||||
});
|
||||
|
||||
// Additional optimizations can be added here
|
||||
// The CSS already handles most responsive adjustments
|
||||
}
|
||||
|
||||
/**
|
||||
* Save fullscreen preference to localStorage and URL
|
||||
*/
|
||||
function saveFullscreenPreference(isFullscreen) {
|
||||
try {
|
||||
localStorage.setItem('attendance_fullscreen_preference', isFullscreen ? 'true' : 'false');
|
||||
|
||||
// Also update the hidden form field if it exists
|
||||
const fullscreenInput = document.getElementById('fullscreenState');
|
||||
if (fullscreenInput) {
|
||||
fullscreenInput.value = isFullscreen ? '1' : '';
|
||||
}
|
||||
|
||||
console.log('Fullscreen preference saved:', isFullscreen);
|
||||
} catch (e) {
|
||||
console.warn('Could not save fullscreen preference:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load fullscreen preference from localStorage
|
||||
*/
|
||||
function loadFullscreenPreference() {
|
||||
try {
|
||||
const preference = localStorage.getItem('attendance_fullscreen_preference');
|
||||
return preference === 'true';
|
||||
} catch (e) {
|
||||
console.warn('Could not load fullscreen preference:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log fullscreen action for analytics
|
||||
*/
|
||||
function logFullscreenAction(action) {
|
||||
const logData = {
|
||||
action: action,
|
||||
timestamp: new Date().toISOString(),
|
||||
viewport: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
orientation: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait'
|
||||
},
|
||||
userAgent: navigator.userAgent,
|
||||
isIPad: /iPad/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
||||
};
|
||||
|
||||
console.log('Fullscreen action logged:', logData);
|
||||
|
||||
// You can send this to your backend for analytics if needed
|
||||
// fetch('/api/log-fullscreen', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(logData)
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle native fullscreen change events (only for desktop)
|
||||
*/
|
||||
function handleFullscreenChange() {
|
||||
// Skip handling on touch devices since we're not using native fullscreen there
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
if (isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNativeFullscreen = !!(
|
||||
document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement
|
||||
);
|
||||
|
||||
// Sync our state with native fullscreen (desktop only)
|
||||
if (!isNativeFullscreen && isFullscreen) {
|
||||
// User exited native fullscreen, update our state
|
||||
const container = document.getElementById('attendanceReportContainer');
|
||||
const icon = document.getElementById('fullscreenIcon');
|
||||
const body = document.body;
|
||||
|
||||
if (container && icon) {
|
||||
isFullscreen = false;
|
||||
exitFullscreen(container, icon, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard shortcuts
|
||||
*/
|
||||
function handleKeyboardShortcuts(event) {
|
||||
// F11 or F for fullscreen toggle
|
||||
if (event.key === 'F11' || (event.key === 'f' && event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleFullscreen();
|
||||
}
|
||||
|
||||
// Escape to exit fullscreen
|
||||
if (event.key === 'Escape' && isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect iPad and adjust UI accordingly
|
||||
*/
|
||||
function detectAndOptimizeForIPad() {
|
||||
const isIPad = /iPad/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 0);
|
||||
|
||||
if (isIPad) {
|
||||
console.log('iPad detected - optimizing UI');
|
||||
document.body.classList.add('ipad-device');
|
||||
|
||||
// Add iPad-specific optimizations
|
||||
const container = document.getElementById('attendanceReportContainer');
|
||||
if (container) {
|
||||
container.classList.add('ipad-optimized');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore fullscreen state from URL (CSS-only, no native fullscreen)
|
||||
*/
|
||||
function restoreFullscreenFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const fullscreenParam = urlParams.get('fullscreen');
|
||||
|
||||
if (fullscreenParam === '1' && !isFullscreen) {
|
||||
console.log('Restoring fullscreen mode from URL parameter (CSS-only)');
|
||||
|
||||
// Use CSS-only fullscreen (no native fullscreen API call)
|
||||
const container = document.getElementById('attendanceReportContainer');
|
||||
const icon = document.getElementById('fullscreenIcon');
|
||||
const body = document.body;
|
||||
|
||||
if (container && icon) {
|
||||
// Manually set fullscreen state without calling native API
|
||||
isFullscreen = true;
|
||||
container.classList.add('fullscreen-mode');
|
||||
body.classList.add('fullscreen-active');
|
||||
|
||||
// Update icon
|
||||
icon.classList.remove('fa-expand');
|
||||
icon.classList.add('fa-compress');
|
||||
|
||||
// Update button
|
||||
const button = document.getElementById('fullscreenToggle');
|
||||
if (button) {
|
||||
button.setAttribute('title', 'Exit Fullscreen');
|
||||
}
|
||||
|
||||
// Apply touch gesture prevention
|
||||
preventSwipeGestures(container, true);
|
||||
|
||||
// Save preference
|
||||
saveFullscreenPreference(true);
|
||||
|
||||
console.log('Fullscreen restored successfully (CSS-only mode)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize fullscreen functionality
|
||||
*/
|
||||
function initializeFullscreen() {
|
||||
console.log('Initializing fullscreen functionality');
|
||||
|
||||
// Detect iPad
|
||||
detectAndOptimizeForIPad();
|
||||
|
||||
// Add event listeners for native fullscreen changes (desktop only)
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
||||
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
||||
|
||||
// Add keyboard shortcuts
|
||||
document.addEventListener('keydown', handleKeyboardShortcuts);
|
||||
|
||||
// Intercept filter form submission to preserve fullscreen state
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
if (filterForm) {
|
||||
filterForm.addEventListener('submit', function(e) {
|
||||
const fullscreenInput = document.getElementById('fullscreenState');
|
||||
if (fullscreenInput) {
|
||||
fullscreenInput.value = isFullscreen ? '1' : '';
|
||||
console.log('Filter form submitted with fullscreen state:', isFullscreen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle orientation changes
|
||||
window.addEventListener('orientationchange', function() {
|
||||
console.log('Orientation changed');
|
||||
if (isFullscreen) {
|
||||
adjustTableForFullscreen(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(function() {
|
||||
if (isFullscreen) {
|
||||
adjustTableForFullscreen(true);
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// Restore fullscreen state from URL if present
|
||||
// Use setTimeout to ensure DOM is fully loaded
|
||||
setTimeout(function() {
|
||||
restoreFullscreenFromURL();
|
||||
}, 100);
|
||||
|
||||
console.log('Fullscreen functionality initialized');
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeFullscreen);
|
||||
} else {
|
||||
initializeFullscreen();
|
||||
}
|
||||
|
||||
// Export functions for external use
|
||||
window.toggleFullscreen = toggleFullscreen;
|
||||
window.isAttendanceFullscreen = function() { return isFullscreen; };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* Unified Dashboard JavaScript for QR Code Management
|
||||
* static/js/dashboard.js
|
||||
*/
|
||||
|
||||
class ProjectDashboardManager {
|
||||
constructor() {
|
||||
this.expandedProjects = new Set();
|
||||
this.currentModalQR = null;
|
||||
this.selectedQRCodes = new Set();
|
||||
this.allExpanded = false;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const saved = localStorage.getItem("expandedProjects");
|
||||
if (saved) {
|
||||
this.expandedProjects = new Set(JSON.parse(saved));
|
||||
this.restoreProjectStates();
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
this.addScrollAnimations();
|
||||
}
|
||||
|
||||
restoreProjectStates() {
|
||||
this.expandedProjects.forEach((projectId) => {
|
||||
this.expandProject(projectId, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
setupEventListeners() {
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// ESC to close modals
|
||||
if (e.key === "Escape") {
|
||||
this.closeQRModal();
|
||||
this.closeImageLightbox();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + F to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
const searchInput = document.getElementById("qrSearch");
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
|
||||
// Delete key for bulk delete (when items selected)
|
||||
if (e.key === "Delete" && this.selectedQRCodes.size > 0) {
|
||||
e.preventDefault();
|
||||
this.bulkDeleteQRCodes();
|
||||
}
|
||||
});
|
||||
|
||||
// Expand/collapse all toggle
|
||||
const expandToggle = document.getElementById("expandAllToggle");
|
||||
if (expandToggle) {
|
||||
expandToggle.addEventListener("click", () => {
|
||||
this.toggleExpandAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add scroll animations for QR items
|
||||
addScrollAnimations() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("animate-in");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const qrItems = document.querySelectorAll(".qr-item, .qr-card");
|
||||
qrItems.forEach((item) => observer.observe(item));
|
||||
}
|
||||
|
||||
// Project Management Functions
|
||||
toggleProject(projectId) {
|
||||
const isExpanded = this.expandedProjects.has(projectId);
|
||||
|
||||
if (isExpanded) {
|
||||
this.collapseProject(projectId);
|
||||
} else {
|
||||
this.expandProject(projectId);
|
||||
}
|
||||
|
||||
this.saveExpandedState();
|
||||
}
|
||||
|
||||
expandProject(projectId, animate = true) {
|
||||
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||
const header = toggle?.closest(".project-header");
|
||||
|
||||
if (projectQR && toggle) {
|
||||
projectQR.classList.add("expanded");
|
||||
toggle.classList.add("expanded");
|
||||
header?.classList.add("expanded");
|
||||
this.expandedProjects.add(projectId);
|
||||
|
||||
if (animate) {
|
||||
setTimeout(() => {
|
||||
projectQR.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collapseProject(projectId) {
|
||||
const projectQR = document.getElementById(`project-qr-${projectId}`);
|
||||
const toggle = document.getElementById(`toggle-${projectId}`);
|
||||
const header = toggle?.closest(".project-header");
|
||||
|
||||
if (projectQR && toggle) {
|
||||
projectQR.classList.remove("expanded");
|
||||
toggle.classList.remove("expanded");
|
||||
header?.classList.remove("expanded");
|
||||
this.expandedProjects.delete(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
saveExpandedState() {
|
||||
localStorage.setItem(
|
||||
"expandedProjects",
|
||||
JSON.stringify([...this.expandedProjects])
|
||||
);
|
||||
}
|
||||
|
||||
// QR Code Status Toggle
|
||||
async toggleQRCodeStatus(qrId) {
|
||||
try {
|
||||
const response = await fetch(`/qr-codes/${qrId}/toggle-status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showToast(result.message, "success");
|
||||
|
||||
// Reload page after short delay to show updated status
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(result.message || "Failed to toggle QR code status");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to toggle QR code status");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle status failed:", error);
|
||||
this.showToast("Failed to update QR code status", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// QR Modal Functions
|
||||
openQRModalFromData(element) {
|
||||
const qrData = {
|
||||
name: element.dataset.qrName,
|
||||
image: element.querySelector("img").src,
|
||||
location: element.dataset.qrLocation,
|
||||
address: element.dataset.qrAddress,
|
||||
event: element.dataset.qrEvent,
|
||||
qr_url: element.dataset.qrUrl,
|
||||
};
|
||||
|
||||
this.openQRModal(qrData);
|
||||
}
|
||||
|
||||
openQRModal(qrData) {
|
||||
const modal = document.getElementById("qrModal");
|
||||
const modalImage = document.getElementById("modalQRImage");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
const modalQRName = document.getElementById("modalQRName");
|
||||
const modalQRLocation = document.getElementById("modalQRLocation");
|
||||
const modalQRAddress = document.getElementById("modalQRAddress");
|
||||
const modalQREvent = document.getElementById("modalQREvent");
|
||||
const modalQRDestination = document.getElementById("modalQRDestination");
|
||||
|
||||
if (modal && modalImage && modalTitle) {
|
||||
modalTitle.textContent = `QR Code: ${qrData.name}`;
|
||||
modalImage.src = qrData.image;
|
||||
modalImage.alt = `QR Code for ${qrData.name}`;
|
||||
|
||||
if (modalQRName) modalQRName.textContent = qrData.name || "-";
|
||||
if (modalQRLocation) modalQRLocation.textContent = qrData.location || "-";
|
||||
if (modalQRAddress) modalQRAddress.textContent = qrData.address || "-";
|
||||
if (modalQREvent)
|
||||
modalQREvent.textContent = qrData.event || "No event specified";
|
||||
|
||||
if (modalQRDestination && qrData.qr_url) {
|
||||
const destinationUrl = `${window.location.origin}/qr/${qrData.qr_url}`;
|
||||
const linkElement = modalQRDestination.querySelector("a");
|
||||
if (linkElement) {
|
||||
linkElement.href = destinationUrl;
|
||||
linkElement.innerHTML = `
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
${destinationUrl}
|
||||
`;
|
||||
}
|
||||
} else if (modalQRDestination) {
|
||||
modalQRDestination.innerHTML =
|
||||
'<span style="color: var(--gray-500); font-style: italic;">No destination URL available</span>';
|
||||
}
|
||||
|
||||
this.currentModalQR = {
|
||||
name: qrData.name,
|
||||
image: qrData.image,
|
||||
location: qrData.location,
|
||||
address: qrData.address,
|
||||
event: qrData.event,
|
||||
qr_url: qrData.qr_url,
|
||||
destination_url: qrData.qr_url
|
||||
? `${window.location.origin}/qr/${qrData.qr_url}`
|
||||
: null,
|
||||
};
|
||||
|
||||
modal.style.display = "flex";
|
||||
|
||||
document.addEventListener("keydown", this.handleModalKeydown.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
closeQRModal() {
|
||||
const modal = document.getElementById("qrModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
this.currentModalQR = null;
|
||||
|
||||
document.removeEventListener(
|
||||
"keydown",
|
||||
this.handleModalKeydown.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleModalKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.closeQRModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Download Functions
|
||||
downloadModalQR() {
|
||||
if (this.currentModalQR) {
|
||||
const base64Data = this.currentModalQR.image.includes("base64,")
|
||||
? this.currentModalQR.image.split("base64,")[1]
|
||||
: this.currentModalQR.image;
|
||||
this.downloadQR(base64Data, this.currentModalQR.name);
|
||||
}
|
||||
}
|
||||
|
||||
downloadQRFromCard(button) {
|
||||
const qrCard = button.closest(".qr-card") || button.closest(".qr-item");
|
||||
const img = qrCard.querySelector("img");
|
||||
const qrName =
|
||||
qrCard.dataset.qrName ||
|
||||
qrCard.querySelector(".qr-name")?.textContent ||
|
||||
"qr_code";
|
||||
|
||||
if (img && img.src) {
|
||||
const base64Data = img.src.includes("base64,")
|
||||
? img.src.split("base64,")[1]
|
||||
: img.src;
|
||||
this.downloadQR(base64Data, qrName);
|
||||
}
|
||||
}
|
||||
|
||||
downloadQR(base64Image, filename) {
|
||||
try {
|
||||
const base64Data = base64Image.includes("base64,")
|
||||
? base64Image.split("base64,")[1]
|
||||
: base64Image;
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = `data:image/png;base64,${base64Data}`;
|
||||
link.download = `${filename
|
||||
.replace(/[^a-z0-9]/gi, "_")
|
||||
.toLowerCase()}_qr_code.png`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showToast("QR code downloaded successfully!", "success");
|
||||
} catch (error) {
|
||||
console.error("Download error:", error);
|
||||
this.showToast("Failed to download QR code", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Copy Functions
|
||||
copyModalQRData() {
|
||||
if (this.currentModalQR) {
|
||||
const data = `QR Code: ${this.currentModalQR.name}\nLocation: ${
|
||||
this.currentModalQR.location
|
||||
}\nAddress: ${this.currentModalQR.address}\nEvent: ${
|
||||
this.currentModalQR.event
|
||||
}${
|
||||
this.currentModalQR.destination_url
|
||||
? `\nQR Link: ${this.currentModalQR.destination_url}`
|
||||
: ""
|
||||
}`;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() => {
|
||||
this.showToast("QR code information copied to clipboard!", "success");
|
||||
})
|
||||
.catch(() => {
|
||||
this.fallbackCopyText(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
copyQRDestination() {
|
||||
if (this.currentModalQR && this.currentModalQR.destination_url) {
|
||||
navigator.clipboard
|
||||
.writeText(this.currentModalQR.destination_url)
|
||||
.then(() => {
|
||||
this.showToast("QR destination link copied to clipboard!", "success");
|
||||
})
|
||||
.catch(() => {
|
||||
this.showToast("Failed to copy QR link", "error");
|
||||
});
|
||||
} else {
|
||||
this.showToast("No QR destination link available", "warning");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Copy QR Code URL
|
||||
async copyQRUrl(qrId) {
|
||||
try {
|
||||
const response = await fetch(`/qr-codes/${qrId}/copy-url`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.url) {
|
||||
// Copy to clipboard
|
||||
await navigator.clipboard.writeText(data.url);
|
||||
this.showToast("QR code URL copied to clipboard!", "success");
|
||||
|
||||
// Log the action
|
||||
console.log(`QR URL copied for ID: ${qrId}`);
|
||||
} else {
|
||||
this.showToast("Failed to copy URL", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Copy URL error:", error);
|
||||
this.showToast("Failed to copy URL", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Open QR Code Link
|
||||
async openQRLink(qrId) {
|
||||
try {
|
||||
const response = await fetch(`/qr-codes/${qrId}/open-link`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.url) {
|
||||
// Open in new tab
|
||||
window.open(data.url, "_blank");
|
||||
this.showToast("QR code link opened!", "success");
|
||||
|
||||
// Log the action
|
||||
console.log(`QR link opened for ID: ${qrId}`);
|
||||
} else {
|
||||
this.showToast("Failed to open link", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Open link error:", error);
|
||||
this.showToast("Failed to open link", "error");
|
||||
}
|
||||
}
|
||||
|
||||
fallbackCopyText(text) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
this.showToast("QR code information copied to clipboard!", "success");
|
||||
} catch (err) {
|
||||
this.showToast("Failed to copy to clipboard", "error");
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Image Lightbox Functions
|
||||
openImageLightbox(previewElement, qrName) {
|
||||
console.log("Opening lightbox for:", qrName); // Debug log
|
||||
|
||||
const img = previewElement.querySelector("img");
|
||||
|
||||
if (img && img.src) {
|
||||
const lightbox = document.getElementById("imageLightbox");
|
||||
const lightboxImage = document.getElementById("lightboxImage");
|
||||
const lightboxInfo = document.getElementById("lightboxInfo");
|
||||
|
||||
console.log(
|
||||
"Lightbox elements found:",
|
||||
!!lightbox,
|
||||
!!lightboxImage,
|
||||
!!lightboxInfo
|
||||
); // Debug log
|
||||
|
||||
if (lightbox && lightboxImage && lightboxInfo) {
|
||||
lightboxImage.src = img.src;
|
||||
lightboxImage.alt = img.alt;
|
||||
lightboxInfo.textContent = `QR Code: ${qrName}`;
|
||||
|
||||
lightbox.style.display = "flex";
|
||||
console.log("Lightbox should be visible now"); // Debug log
|
||||
|
||||
// Add keyboard listener for ESC key
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
this.handleLightboxKeydown.bind(this)
|
||||
);
|
||||
} else {
|
||||
console.error("Lightbox elements not found");
|
||||
}
|
||||
} else {
|
||||
console.error("Image element not found or no src");
|
||||
}
|
||||
}
|
||||
|
||||
closeImageLightbox() {
|
||||
console.log("Closing lightbox"); // Debug log
|
||||
const lightbox = document.getElementById("imageLightbox");
|
||||
if (lightbox) {
|
||||
lightbox.style.display = "none";
|
||||
|
||||
// Remove keyboard listener
|
||||
document.removeEventListener(
|
||||
"keydown",
|
||||
this.handleLightboxKeydown.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleLightboxKeydown(event) {
|
||||
if (event.key === "Escape") {
|
||||
this.closeImageLightbox();
|
||||
}
|
||||
}
|
||||
|
||||
// QR Item Toggle functionality (for selection)
|
||||
toggleQRItem(element) {
|
||||
const qrId = element.dataset.qrId;
|
||||
if (this.selectedQRCodes.has(qrId)) {
|
||||
this.selectedQRCodes.delete(qrId);
|
||||
element.classList.remove("selected");
|
||||
} else {
|
||||
this.selectedQRCodes.add(qrId);
|
||||
element.classList.add("selected");
|
||||
}
|
||||
|
||||
// Update bulk action buttons if they exist
|
||||
this.updateBulkActionButtons();
|
||||
}
|
||||
|
||||
updateBulkActionButtons() {
|
||||
const bulkActions = document.querySelector(".bulk-actions");
|
||||
if (bulkActions) {
|
||||
bulkActions.style.display =
|
||||
this.selectedQRCodes.size > 0 ? "flex" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Copy QR data functionality
|
||||
copyQRData(name, location, address, event) {
|
||||
const data = `QR Code: ${name}\nLocation: ${location}\nAddress: ${address}\nEvent: ${event}`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(data)
|
||||
.then(() =>
|
||||
this.showToast("QR code information copied to clipboard!", "success")
|
||||
)
|
||||
.catch(() => this.fallbackCopyText(data));
|
||||
} else {
|
||||
this.fallbackCopyText(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete QR Code
|
||||
async deleteQRCode(qrId, qrName) {
|
||||
if (!confirm(`Delete "${qrName}"? This cannot be undone.`)) return;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
const deleteBtn = document.querySelector(
|
||||
`[onclick*="deleteQRCode(${qrId}"]`
|
||||
);
|
||||
if (deleteBtn) {
|
||||
deleteBtn.disabled = true;
|
||||
deleteBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/qr-codes/${qrId}/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Remove QR item from page immediately
|
||||
const qrItem = document.querySelector(`[data-qr-id="${qrId}"]`);
|
||||
if (qrItem) {
|
||||
qrItem.style.transition = "opacity 0.3s";
|
||||
qrItem.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
qrItem.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
this.showToast(`QR code "${qrName}" deleted successfully!`, "success");
|
||||
} else {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Delete error:", error);
|
||||
|
||||
// Restore button if there was an error
|
||||
const deleteBtn = document.querySelector(
|
||||
`[onclick*="deleteQRCode(${qrId}"]`
|
||||
);
|
||||
if (deleteBtn) {
|
||||
deleteBtn.disabled = false;
|
||||
deleteBtn.innerHTML = '<i class="fas fa-trash"></i>';
|
||||
}
|
||||
|
||||
// Show error message
|
||||
this.showToast("Failed to delete QR code. Please try again.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Toast notification system
|
||||
showToast(message, type = "info") {
|
||||
const toast = document.createElement("div");
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${
|
||||
type === "success"
|
||||
? "#10b981"
|
||||
: type === "error"
|
||||
? "#ef4444"
|
||||
: type === "warning"
|
||||
? "#f59e0b"
|
||||
: "#3b82f6"
|
||||
};
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
z-index: 9999;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
`;
|
||||
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateX(0)";
|
||||
}, 100);
|
||||
|
||||
// Animate out
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateX(100%)";
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
toast.remove();
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Expand/collapse functionality
|
||||
toggleExpandAll() {
|
||||
this.allExpanded = !this.allExpanded;
|
||||
const qrItems = document.querySelectorAll(".qr-item");
|
||||
const expandToggle = document.getElementById("expandAllToggle");
|
||||
|
||||
qrItems.forEach((item) => {
|
||||
const details = item.querySelector(".qr-details");
|
||||
if (details) {
|
||||
if (this.allExpanded) {
|
||||
details.style.display = "block";
|
||||
item.classList.add("expanded");
|
||||
} else {
|
||||
details.style.display = "none";
|
||||
item.classList.remove("expanded");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (expandToggle) {
|
||||
expandToggle.innerHTML = this.allExpanded
|
||||
? '<i class="fas fa-compress-alt"></i> Collapse All'
|
||||
: '<i class="fas fa-expand-alt"></i> Expand All';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global variable to hold dashboard manager instance
|
||||
let dashboardManager;
|
||||
|
||||
// Initialize dashboard when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
dashboardManager = new ProjectDashboardManager();
|
||||
|
||||
// Register all global functions for template compatibility
|
||||
window.toggleProject = (projectId) =>
|
||||
dashboardManager.toggleProject(projectId);
|
||||
window.toggleQRCodeStatus = (qrId) =>
|
||||
dashboardManager.toggleQRCodeStatus(qrId);
|
||||
window.openQRModalFromData = (element) =>
|
||||
dashboardManager.openQRModalFromData(element);
|
||||
window.openQRModal = (qrData) => dashboardManager.openQRModal(qrData);
|
||||
window.closeQRModal = () => dashboardManager.closeQRModal();
|
||||
window.downloadModalQR = () => dashboardManager.downloadModalQR();
|
||||
window.copyModalQRData = () => dashboardManager.copyModalQRData();
|
||||
window.copyQRDestination = () => dashboardManager.copyQRDestination();
|
||||
window.downloadQRFromCard = (button) =>
|
||||
dashboardManager.downloadQRFromCard(button);
|
||||
window.openImageLightbox = (element, qrName) =>
|
||||
dashboardManager.openImageLightbox(element, qrName);
|
||||
window.closeImageLightbox = () => dashboardManager.closeImageLightbox();
|
||||
window.toggleQRItem = (element) => dashboardManager.toggleQRItem(element);
|
||||
window.copyQRData = (name, location, address, event) =>
|
||||
dashboardManager.copyQRData(name, location, address, event);
|
||||
window.deleteQRCode = (qrId, qrName) =>
|
||||
dashboardManager.deleteQRCode(qrId, qrName);
|
||||
|
||||
// FIXED: Global functions for copy/open link functionality
|
||||
window.copyQRUrl = function (qrId) {
|
||||
if (dashboardManager && dashboardManager.copyQRUrl) {
|
||||
dashboardManager.copyQRUrl(qrId);
|
||||
} else {
|
||||
console.error(
|
||||
"ProjectDashboardManager not initialized or copyQRUrl method missing"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.openQRLink = function (qrId) {
|
||||
if (dashboardManager && dashboardManager.openQRLink) {
|
||||
dashboardManager.openQRLink(qrId);
|
||||
} else {
|
||||
console.error(
|
||||
"ProjectDashboardManager not initialized or openQRLink method missing"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(
|
||||
"Project Dashboard initialized successfully with copy/open link functionality"
|
||||
);
|
||||
console.log("Dashboard keyboard shortcuts:");
|
||||
console.log("Ctrl/Cmd + F: Focus search");
|
||||
console.log("Escape: Close modal");
|
||||
});
|
||||
@@ -0,0 +1,862 @@
|
||||
/**
|
||||
* Enhanced Export Configuration JavaScript with Drag & Drop
|
||||
* Handles column selection, preview updates, drag & drop reordering, and preference management
|
||||
*/
|
||||
|
||||
// Default column configuration
|
||||
const DEFAULT_COLUMN_ORDER = [
|
||||
'employee_id', // ID
|
||||
'device_info', // Platform
|
||||
'check_in_date', // Date
|
||||
'check_in_time', // Time
|
||||
'location_name', // Location Name
|
||||
'status', // Action Description
|
||||
'qr_address', // Event Description
|
||||
'address', // Recorded Address
|
||||
'location_accuracy' // Distance
|
||||
];
|
||||
|
||||
const DEFAULT_COLUMNS = {
|
||||
'employee_id': { selected: true, name: 'ID' },
|
||||
'location_name': { selected: true, name: 'Location Name' },
|
||||
'status': { selected: true, name: 'Action Description' },
|
||||
'check_in_date': { selected: true, name: 'Date' },
|
||||
'check_in_time': { selected: true, name: 'Time' },
|
||||
'qr_address': { selected: true, name: 'Event Description' },
|
||||
'address': { selected: true, name: 'Recorded Address' },
|
||||
'device_info': { selected: true, name: 'Platform' },
|
||||
'location_accuracy': { selected: true, name: 'Distance' }
|
||||
};
|
||||
|
||||
// Global variables
|
||||
let availableColumns = [];
|
||||
let sortableInstance = null;
|
||||
let savedColumnOrder = [];
|
||||
|
||||
function toggleSection(sectionId) {
|
||||
try {
|
||||
const section = document.getElementById(sectionId);
|
||||
const icon = document.getElementById(sectionId + 'Icon');
|
||||
|
||||
if (!section) return;
|
||||
|
||||
if (section.classList.contains('collapsed')) {
|
||||
section.classList.remove('collapsed');
|
||||
if (icon) icon.classList.remove('rotated');
|
||||
} else {
|
||||
section.classList.add('collapsed');
|
||||
if (icon) icon.classList.add('rotated');
|
||||
}
|
||||
|
||||
// Save state
|
||||
try {
|
||||
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
|
||||
collapsedSections[sectionId] = section.classList.contains('collapsed');
|
||||
localStorage.setItem('collapsedSections', JSON.stringify(collapsedSections));
|
||||
} catch (e) {}
|
||||
} catch (error) {
|
||||
console.error('Error toggling section:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreCollapsedState() {
|
||||
try {
|
||||
const collapsedSections = JSON.parse(localStorage.getItem('collapsedSections') || '{}');
|
||||
|
||||
Object.keys(collapsedSections).forEach(sectionId => {
|
||||
if (collapsedSections[sectionId]) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const icon = document.getElementById(sectionId + 'Icon');
|
||||
|
||||
if (section) {
|
||||
section.classList.add('collapsed');
|
||||
if (icon) icon.classList.add('rotated');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Enhanced Export Configuration with Drag & Drop initialized');
|
||||
|
||||
// Initialize available columns data
|
||||
initializeColumnsData();
|
||||
|
||||
// Set up event listeners
|
||||
setupEventListeners();
|
||||
|
||||
// Load saved preferences if available
|
||||
loadSavedPreferences();
|
||||
|
||||
// Update preview on load
|
||||
updatePreview();
|
||||
|
||||
// Initialize drag & drop
|
||||
initializeDragDrop();
|
||||
});
|
||||
|
||||
function initializeColumnsData() {
|
||||
try {
|
||||
// Extract column data from the form
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
availableColumns = Array.from(checkboxes).map(cb => {
|
||||
const columnKey = cb.value;
|
||||
const label = cb.parentElement.querySelector('label').textContent.trim();
|
||||
const nameInput = document.getElementById('name_' + columnKey);
|
||||
|
||||
return {
|
||||
key: columnKey,
|
||||
label: label,
|
||||
defaultName: nameInput ? nameInput.value : label,
|
||||
enabled: cb.checked
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Initialized columns data:', availableColumns);
|
||||
} catch (error) {
|
||||
console.error('Error initializing columns data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
try {
|
||||
// Add change listeners to all column checkboxes
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
toggleColumnName(this.value);
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
|
||||
// Add visual feedback
|
||||
const columnItem = this.closest('.column-item');
|
||||
if (this.checked) {
|
||||
columnItem.classList.add('selected');
|
||||
} else {
|
||||
columnItem.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add input listeners to all column name inputs
|
||||
const nameInputs = document.querySelectorAll('input[id^="name_"]');
|
||||
nameInputs.forEach(input => {
|
||||
input.addEventListener('input', debounce(() => {
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
}, 300));
|
||||
});
|
||||
|
||||
// Form validation before submit
|
||||
const form = document.getElementById('exportForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up event listeners:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleColumnName(columnKey) {
|
||||
try {
|
||||
const checkbox = document.getElementById('col_' + columnKey);
|
||||
const nameGroup = document.getElementById('name_group_' + columnKey);
|
||||
|
||||
if (checkbox && nameGroup) {
|
||||
if (checkbox.checked) {
|
||||
nameGroup.style.display = 'block';
|
||||
nameGroup.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
nameGroup.style.opacity = '1';
|
||||
}, 10);
|
||||
} else {
|
||||
nameGroup.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
nameGroup.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling column name:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle edit mode for column name input
|
||||
* @param {string} columnKey - The column key
|
||||
* @param {boolean} enableEdit - Whether to enable editing
|
||||
*/
|
||||
function toggleEditMode(columnKey, enableEdit) {
|
||||
try {
|
||||
const nameInput = document.getElementById('name_' + columnKey);
|
||||
|
||||
if (nameInput) {
|
||||
if (enableEdit) {
|
||||
// Enable editing
|
||||
nameInput.removeAttribute('readonly');
|
||||
nameInput.focus();
|
||||
nameInput.select();
|
||||
console.log(`✏️ Editing enabled for column: ${columnKey}`);
|
||||
} else {
|
||||
// Disable editing
|
||||
nameInput.setAttribute('readonly', 'readonly');
|
||||
console.log(`🔒 Editing disabled for column: ${columnKey}`);
|
||||
}
|
||||
|
||||
// Update preview when name changes
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling edit mode:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initializeDragDrop() {
|
||||
try {
|
||||
const selectedColumnsList = document.getElementById('selectedColumnsList');
|
||||
if (selectedColumnsList) {
|
||||
sortableInstance = Sortable.create(selectedColumnsList, {
|
||||
animation: 200,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
handle: '.column-drag-handle',
|
||||
onStart: function(evt) {
|
||||
console.log('Drag started:', evt.oldIndex);
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
console.log('Drag ended:', evt.oldIndex, '->', evt.newIndex);
|
||||
updateColumnOrderNumbers();
|
||||
updatePreview();
|
||||
|
||||
// Save the new order
|
||||
savePreferences();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Drag & drop initialized successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing drag & drop:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelectedColumnsList() {
|
||||
try {
|
||||
const selectedColumnsList = document.getElementById('selectedColumnsList');
|
||||
const selectedColumnsSection = document.getElementById('selectedColumnsSection');
|
||||
|
||||
if (!selectedColumnsList || !selectedColumnsSection) return;
|
||||
|
||||
// Get currently selected columns
|
||||
const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked'));
|
||||
|
||||
if (selectedColumns.length === 0) {
|
||||
selectedColumnsSection.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedColumnsSection.style.display = 'block';
|
||||
selectedColumnsSection.classList.add('has-columns');
|
||||
|
||||
// Get current order if exists, otherwise use selection order
|
||||
let orderedColumns = [];
|
||||
if (savedColumnOrder.length > 0) {
|
||||
// Use saved order, but only include currently selected columns
|
||||
orderedColumns = savedColumnOrder.filter(key =>
|
||||
selectedColumns.some(cb => cb.value === key)
|
||||
);
|
||||
// Add any newly selected columns that weren't in saved order
|
||||
selectedColumns.forEach(cb => {
|
||||
if (!orderedColumns.includes(cb.value)) {
|
||||
orderedColumns.push(cb.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
orderedColumns = selectedColumns.map(cb => cb.value);
|
||||
}
|
||||
|
||||
// Build the selected columns list HTML
|
||||
let listHTML = '';
|
||||
orderedColumns.forEach((columnKey, index) => {
|
||||
const nameInput = document.getElementById('name_' + columnKey);
|
||||
const columnData = availableColumns.find(col => col.key === columnKey);
|
||||
const customName = nameInput ? nameInput.value : (columnData ? columnData.label : columnKey);
|
||||
|
||||
listHTML += `
|
||||
<div class="selected-column-item" data-column-key="${columnKey}">
|
||||
<div class="selected-column-info">
|
||||
<div class="column-drag-handle" title="Drag to reorder">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</div>
|
||||
<div class="selected-column-details">
|
||||
<div class="selected-column-name">${columnData ? columnData.label : columnKey}</div>
|
||||
<div class="selected-column-export-name">Export as: "${customName}"</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column-order-number">${index + 1}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (listHTML === '') {
|
||||
listHTML = `
|
||||
<div class="selected-columns-empty">
|
||||
<i class="fas fa-hand-point-up"></i>
|
||||
<p>Select columns above to see them here for reordering</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
selectedColumnsList.innerHTML = listHTML;
|
||||
|
||||
// Re-initialize sortable after updating content
|
||||
if (sortableInstance) {
|
||||
sortableInstance.destroy();
|
||||
}
|
||||
initializeDragDrop();
|
||||
|
||||
console.log(`Updated selected columns list with ${orderedColumns.length} columns`);
|
||||
} catch (error) {
|
||||
console.error('Error updating selected columns list:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateColumnOrderNumbers() {
|
||||
try {
|
||||
const orderNumbers = document.querySelectorAll('.column-order-number');
|
||||
orderNumbers.forEach((element, index) => {
|
||||
element.textContent = index + 1;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating column order numbers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentColumnOrder() {
|
||||
try {
|
||||
const selectedItems = document.querySelectorAll('.selected-column-item');
|
||||
return Array.from(selectedItems).map(item => item.dataset.columnKey);
|
||||
} catch (error) {
|
||||
console.error('Error getting current column order:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateColumnOrderField() {
|
||||
try {
|
||||
const columnOrderField = document.getElementById('column_order');
|
||||
const currentOrder = getCurrentColumnOrder();
|
||||
if (columnOrderField) {
|
||||
columnOrderField.value = JSON.stringify(currentOrder);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating column order field:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
try {
|
||||
const previewHeader = document.getElementById('previewHeader');
|
||||
const previewTable = document.querySelector('.preview-table tbody');
|
||||
|
||||
if (!previewHeader || !previewTable) {
|
||||
console.warn('Preview elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected columns in their current order
|
||||
const currentOrder = getCurrentColumnOrder();
|
||||
|
||||
if (currentOrder.length === 0) {
|
||||
// No columns selected
|
||||
previewHeader.innerHTML = '';
|
||||
previewTable.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="100%" class="preview-placeholder">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
No columns selected - please select at least one column to export
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
updateGenerateButton(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build header with order numbers
|
||||
let headerHTML = '';
|
||||
currentOrder.forEach((columnKey, index) => {
|
||||
const nameInput = document.getElementById('name_' + columnKey);
|
||||
const customName = nameInput ? nameInput.value : columnKey;
|
||||
|
||||
headerHTML += `<th data-order="${index + 1}">${customName}</th>`;
|
||||
});
|
||||
previewHeader.innerHTML = headerHTML;
|
||||
|
||||
// Build sample data row
|
||||
let sampleRowHTML = '<tr>';
|
||||
currentOrder.forEach(columnKey => {
|
||||
let sampleData = getSampleData(columnKey);
|
||||
|
||||
// Special handling for address column to show the logic
|
||||
if (columnKey === 'address') {
|
||||
sampleData = `<span title="If location accuracy ≤ 0.5 miles: shows QR address, otherwise: shows actual check-in address">123 Business St, City*</span>`;
|
||||
}
|
||||
|
||||
sampleRowHTML += `<td>${sampleData}</td>`;
|
||||
});
|
||||
sampleRowHTML += '</tr>';
|
||||
|
||||
// Add explanation row if address column is selected
|
||||
const hasAddressColumn = currentOrder.includes('address');
|
||||
if (hasAddressColumn) {
|
||||
sampleRowHTML += `
|
||||
<tr style="background-color: #f8f9fa; font-size: 0.85em; color: #6c757d;">
|
||||
<td colspan="${currentOrder.length}" style="text-align: center; padding: 0.75rem; font-style: italic;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
* Check-in Address: Shows QR address when location accuracy ≤ 0.5 miles, otherwise shows actual GPS address
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
previewTable.innerHTML = sampleRowHTML;
|
||||
|
||||
// Update generate button
|
||||
updateGenerateButton(currentOrder.length);
|
||||
|
||||
console.log(`Preview updated with ${currentOrder.length} columns in order:`, currentOrder);
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function getSampleData(columnKey) {
|
||||
// Return sample data for each column type
|
||||
const sampleData = {
|
||||
'employee_id': 'EMP001',
|
||||
'location_name': 'Main Office',
|
||||
'status': 'Check In',
|
||||
'check_in_date': '2025-08-14',
|
||||
'check_in_time': '09:30:00',
|
||||
'qr_address': '123 Business St, City',
|
||||
'address': '123 Business St, City',
|
||||
'device_info': 'iPhone 14 Pro',
|
||||
'ip_address': '192.168.1.100',
|
||||
'user_agent': 'Mobile Safari',
|
||||
'latitude': '40.7128',
|
||||
'longitude': '-74.0060',
|
||||
'accuracy': '5.2',
|
||||
'location_accuracy': '0.003'
|
||||
};
|
||||
|
||||
return sampleData[columnKey] || 'Sample Data';
|
||||
}
|
||||
|
||||
function selectAllColumns() {
|
||||
try {
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (!checkbox.checked) {
|
||||
checkbox.checked = true;
|
||||
checkbox.closest('.column-item').classList.add('selected');
|
||||
toggleColumnName(checkbox.value);
|
||||
}
|
||||
});
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
|
||||
console.log('All columns selected');
|
||||
} catch (error) {
|
||||
console.error('Error selecting all columns:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function deselectAllColumns() {
|
||||
try {
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.checked) {
|
||||
checkbox.checked = false;
|
||||
checkbox.closest('.column-item').classList.remove('selected');
|
||||
toggleColumnName(checkbox.value);
|
||||
}
|
||||
});
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
|
||||
console.log('All columns deselected');
|
||||
} catch (error) {
|
||||
console.error('Error deselecting all columns:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function resetToDefaults() {
|
||||
try {
|
||||
console.log('🔄 Resetting to default settings...');
|
||||
|
||||
// First, deselect all columns and reset to defaults
|
||||
const allCheckboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
allCheckboxes.forEach(checkbox => {
|
||||
const key = checkbox.value;
|
||||
const nameInput = document.getElementById('name_' + key);
|
||||
const editCheckbox = document.getElementById('edit_' + key);
|
||||
const columnItem = checkbox.closest('.column-item');
|
||||
|
||||
// Check if this column should be selected by default
|
||||
const isDefaultSelected = DEFAULT_COLUMNS.hasOwnProperty(key) && DEFAULT_COLUMNS[key].selected;
|
||||
|
||||
// Set checkbox state
|
||||
checkbox.checked = isDefaultSelected;
|
||||
|
||||
// Update visual state
|
||||
if (isDefaultSelected) {
|
||||
columnItem?.classList.add('selected');
|
||||
} else {
|
||||
columnItem?.classList.remove('selected');
|
||||
}
|
||||
|
||||
// Set the export name and reset to readonly
|
||||
if (nameInput) {
|
||||
if (DEFAULT_COLUMNS[key]) {
|
||||
nameInput.value = DEFAULT_COLUMNS[key].name;
|
||||
} else {
|
||||
// Use the original default name from availableColumns if not in DEFAULT_COLUMNS
|
||||
const columnData = availableColumns.find(col => col.key === key);
|
||||
nameInput.value = columnData ? columnData.defaultName : nameInput.value;
|
||||
}
|
||||
|
||||
// Reset to readonly mode
|
||||
nameInput.setAttribute('readonly', 'readonly');
|
||||
}
|
||||
|
||||
// Uncheck edit checkbox
|
||||
if (editCheckbox) {
|
||||
editCheckbox.checked = false;
|
||||
}
|
||||
|
||||
// Toggle visibility
|
||||
toggleColumnName(key);
|
||||
});
|
||||
|
||||
// Set the saved order to the default order
|
||||
savedColumnOrder = [...DEFAULT_COLUMN_ORDER];
|
||||
|
||||
// Update the display with the default order
|
||||
updateSelectedColumnsList();
|
||||
|
||||
// Apply the default order to the selected columns list
|
||||
applyDefaultOrder();
|
||||
|
||||
// Update preview
|
||||
updatePreview();
|
||||
|
||||
// Clear saved preferences from localStorage
|
||||
try {
|
||||
localStorage.removeItem('exportPreferences');
|
||||
} catch (storageError) {
|
||||
console.warn('Could not clear saved preferences:', storageError);
|
||||
}
|
||||
|
||||
console.log('✅ Reset to default settings complete');
|
||||
} catch (error) {
|
||||
console.error('❌ Error resetting to defaults:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the default column order to the selected columns list
|
||||
*/
|
||||
function applyDefaultOrder() {
|
||||
try {
|
||||
const selectedList = document.getElementById('selectedColumnsList');
|
||||
if (!selectedList) return;
|
||||
|
||||
const items = Array.from(selectedList.children);
|
||||
if (items.length === 0) return;
|
||||
|
||||
// Sort items based on DEFAULT_COLUMN_ORDER
|
||||
items.sort((a, b) => {
|
||||
const keyA = a.dataset.columnKey;
|
||||
const keyB = b.dataset.columnKey;
|
||||
const indexA = DEFAULT_COLUMN_ORDER.indexOf(keyA);
|
||||
const indexB = DEFAULT_COLUMN_ORDER.indexOf(keyB);
|
||||
|
||||
// If key not in default order, put it at the end
|
||||
if (indexA === -1 && indexB === -1) return 0;
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
// Clear and re-append in sorted order
|
||||
selectedList.innerHTML = '';
|
||||
items.forEach(item => selectedList.appendChild(item));
|
||||
|
||||
// Update order numbers
|
||||
updateColumnOrderNumbers();
|
||||
|
||||
// Update the hidden column order field
|
||||
updateColumnOrderField();
|
||||
|
||||
console.log('📋 Applied default column order');
|
||||
} catch (error) {
|
||||
console.error('Error applying default order:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateGenerateButton(columnCount) {
|
||||
try {
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
if (!generateBtn) return;
|
||||
|
||||
if (columnCount === 0) {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Select columns to export';
|
||||
generateBtn.classList.add('btn-disabled');
|
||||
} else {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = `<i class="fas fa-download"></i> Generate Excel Export (${columnCount} columns)`;
|
||||
generateBtn.classList.remove('btn-disabled');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating generate button:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
try {
|
||||
const selectedColumns = document.querySelectorAll('input[name="selected_columns"]:checked');
|
||||
|
||||
if (selectedColumns.length === 0) {
|
||||
alert('Please select at least one column to export.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate that all selected columns have names
|
||||
let hasEmptyNames = false;
|
||||
selectedColumns.forEach(checkbox => {
|
||||
const nameInput = document.getElementById('name_' + checkbox.value);
|
||||
if (nameInput && nameInput.value.trim() === '') {
|
||||
hasEmptyNames = true;
|
||||
nameInput.style.borderColor = '#e53e3e';
|
||||
nameInput.focus();
|
||||
} else if (nameInput) {
|
||||
nameInput.style.borderColor = '#e2e8f0';
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEmptyNames) {
|
||||
alert('Please provide names for all selected columns.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update column order field before submitting
|
||||
updateColumnOrderField();
|
||||
|
||||
// Save preferences before submitting
|
||||
savePreferences();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error validating form:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function savePreferences() {
|
||||
try {
|
||||
const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked'))
|
||||
.map(cb => cb.value);
|
||||
|
||||
const columnNames = {};
|
||||
const editStates = {};
|
||||
selectedColumns.forEach(col => {
|
||||
const input = document.getElementById('name_' + col);
|
||||
const editCheckbox = document.getElementById('edit_' + col);
|
||||
|
||||
if (input) {
|
||||
columnNames[col] = input.value.trim();
|
||||
}
|
||||
|
||||
if (editCheckbox) {
|
||||
editStates[col] = editCheckbox.checked;
|
||||
}
|
||||
});
|
||||
|
||||
// Get current column order
|
||||
const columnOrder = getCurrentColumnOrder();
|
||||
|
||||
const prefs = {
|
||||
selected_columns: selectedColumns,
|
||||
column_names: columnNames,
|
||||
edit_states: editStates,
|
||||
column_order: columnOrder,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
localStorage.setItem('exportPreferences', JSON.stringify(prefs));
|
||||
console.log('💾 Preferences saved with column order and edit states:', prefs);
|
||||
|
||||
} catch (e) {
|
||||
console.warn('Could not save preferences:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSavedPreferences() {
|
||||
try {
|
||||
const savedPrefs = localStorage.getItem('exportPreferences');
|
||||
if (!savedPrefs) {
|
||||
console.log('No saved preferences found');
|
||||
// Check if there are already selected columns on page load and update preview
|
||||
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
|
||||
if (alreadySelected.length > 0) {
|
||||
console.log('Found pre-selected columns, updating preview');
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const prefs = JSON.parse(savedPrefs);
|
||||
|
||||
// Check if preferences are not too old (30 days)
|
||||
const savedDate = new Date(prefs.timestamp || 0);
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (savedDate < thirtyDaysAgo) {
|
||||
localStorage.removeItem('exportPreferences');
|
||||
console.log('Saved preferences are too old, removed');
|
||||
// Check if there are already selected columns on page load and update preview
|
||||
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
|
||||
if (alreadySelected.length > 0) {
|
||||
console.log('Found pre-selected columns, updating preview');
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply saved column selections
|
||||
if (prefs.selected_columns) {
|
||||
const checkboxes = document.querySelectorAll('input[name="selected_columns"]');
|
||||
checkboxes.forEach(cb => {
|
||||
const shouldBeChecked = prefs.selected_columns.includes(cb.value);
|
||||
if (cb.checked !== shouldBeChecked) {
|
||||
cb.checked = shouldBeChecked;
|
||||
const columnItem = cb.closest('.column-item');
|
||||
if (shouldBeChecked) {
|
||||
columnItem?.classList.add('selected');
|
||||
} else {
|
||||
columnItem?.classList.remove('selected');
|
||||
}
|
||||
toggleColumnName(cb.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply saved column names
|
||||
if (prefs.column_names) {
|
||||
Object.keys(prefs.column_names).forEach(key => {
|
||||
const input = document.getElementById('name_' + key);
|
||||
if (input && prefs.column_names[key]) {
|
||||
input.value = prefs.column_names[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply saved edit states
|
||||
if (prefs.edit_states) {
|
||||
Object.keys(prefs.edit_states).forEach(key => {
|
||||
const editCheckbox = document.getElementById('edit_' + key);
|
||||
const nameInput = document.getElementById('name_' + key);
|
||||
|
||||
if (editCheckbox && prefs.edit_states[key]) {
|
||||
editCheckbox.checked = true;
|
||||
if (nameInput) {
|
||||
nameInput.removeAttribute('readonly');
|
||||
}
|
||||
} else if (nameInput) {
|
||||
nameInput.setAttribute('readonly', 'readonly');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save column order for later use
|
||||
if (prefs.column_order) {
|
||||
savedColumnOrder = prefs.column_order;
|
||||
}
|
||||
|
||||
console.log('📋 Preferences loaded:', prefs);
|
||||
|
||||
// Update preview after loading preferences
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
|
||||
} catch (e) {
|
||||
console.warn('Could not load saved preferences:', e);
|
||||
try {
|
||||
localStorage.removeItem('exportPreferences');
|
||||
} catch (removeError) {
|
||||
console.warn('Could not remove invalid preferences:', removeError);
|
||||
}
|
||||
|
||||
// Check if there are already selected columns on page load and update preview
|
||||
const alreadySelected = document.querySelectorAll('input[name="selected_columns"]:checked');
|
||||
if (alreadySelected.length > 0) {
|
||||
console.log('Found pre-selected columns after preference error, updating preview');
|
||||
updateSelectedColumnsList();
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function for debouncing
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Global functions for template usage
|
||||
window.selectAllColumns = selectAllColumns;
|
||||
window.deselectAllColumns = deselectAllColumns;
|
||||
window.resetToDefaults = resetToDefaults;
|
||||
window.toggleColumnName = toggleColumnName;
|
||||
window.toggleEditMode = toggleEditMode;
|
||||
window.updateSelectedColumnsList = updateSelectedColumnsList;
|
||||
window.getCurrentColumnOrder = getCurrentColumnOrder;
|
||||
window.updateColumnOrderField = updateColumnOrderField;
|
||||
window.savePreferences = savePreferences;
|
||||
|
||||
// Add CSS for disabled button
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.btn-disabled {
|
||||
opacity: 0.6 !important;
|
||||
cursor: not-allowed !important;
|
||||
background: #a0aec0 !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-disabled:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,613 @@
|
||||
// Enhanced JavaScript for Left Sidebar Navigation
|
||||
class QRManager {
|
||||
constructor() {
|
||||
this.initializeApp();
|
||||
}
|
||||
|
||||
initializeApp() {
|
||||
this.initSidebar();
|
||||
this.initModals();
|
||||
this.initDropdowns();
|
||||
this.initActiveNavigation();
|
||||
this.initFlashMessages();
|
||||
}
|
||||
|
||||
// Sidebar Management
|
||||
initSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const sidebarToggle = document.getElementById('sidebarToggle');
|
||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
// Desktop sidebar toggle
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener('click', () => {
|
||||
this.toggleSidebar();
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
if (mobileMenuBtn) {
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
this.toggleMobileSidebar();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile sidebar when clicking overlay
|
||||
if (sidebarOverlay) {
|
||||
sidebarOverlay.addEventListener('click', () => {
|
||||
this.closeMobileSidebar();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile sidebar when clicking menu items
|
||||
const menuItems = sidebar.querySelectorAll('.menu-item');
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 768) {
|
||||
this.closeMobileSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.handleResize();
|
||||
});
|
||||
|
||||
// Initialize sidebar state based on screen size
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
this.saveSidebarState();
|
||||
}
|
||||
}
|
||||
|
||||
toggleMobileSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
const mobileBtn = document.getElementById('mobileMenuBtn');
|
||||
|
||||
if (sidebar && overlay && mobileBtn) {
|
||||
const isOpen = sidebar.classList.contains('mobile-open');
|
||||
|
||||
if (isOpen) {
|
||||
this.closeMobileSidebar();
|
||||
} else {
|
||||
this.openMobileSidebar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openMobileSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
const mobileBtn = document.getElementById('mobileMenuBtn');
|
||||
|
||||
if (sidebar && overlay && mobileBtn) {
|
||||
sidebar.classList.add('mobile-open');
|
||||
overlay.classList.add('active');
|
||||
mobileBtn.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
closeMobileSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
const mobileBtn = document.getElementById('mobileMenuBtn');
|
||||
|
||||
if (sidebar && overlay && mobileBtn) {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
overlay.classList.remove('active');
|
||||
mobileBtn.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar) return;
|
||||
|
||||
if (window.innerWidth <= 768) {
|
||||
// Mobile: ensure sidebar is hidden and mobile menu is available
|
||||
this.closeMobileSidebar();
|
||||
} else if (window.innerWidth <= 1024) {
|
||||
// Tablet: auto-collapse sidebar
|
||||
sidebar.classList.add('collapsed');
|
||||
this.closeMobileSidebar();
|
||||
} else {
|
||||
// Desktop: restore saved state
|
||||
this.restoreSidebarState();
|
||||
this.closeMobileSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
saveSidebarState() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar && window.innerWidth > 1024) {
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||
}
|
||||
}
|
||||
|
||||
restoreSidebarState() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar && window.innerWidth > 1024) {
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
sidebar.classList.toggle('collapsed', isCollapsed);
|
||||
}
|
||||
}
|
||||
|
||||
// Active Navigation Highlighting
|
||||
initActiveNavigation() {
|
||||
const menuItems = document.querySelectorAll('.menu-item[href]');
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const href = item.getAttribute('href');
|
||||
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Theme Management
|
||||
initTheme() {
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
if (!themeToggle) return;
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
this.setTheme(savedTheme);
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const currentTheme = document.body.getAttribute('data-theme') || 'light';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
this.setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
});
|
||||
}
|
||||
|
||||
setTheme(theme) {
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
|
||||
if (themeToggle) {
|
||||
const icon = themeToggle.querySelector('i');
|
||||
const text = themeToggle.querySelector('.menu-text');
|
||||
|
||||
if (theme === 'dark') {
|
||||
icon.className = 'fas fa-sun';
|
||||
if (text) text.textContent = 'Light Mode';
|
||||
} else {
|
||||
icon.className = 'fas fa-moon';
|
||||
if (text) text.textContent = 'Dark Mode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Management
|
||||
initModals() {
|
||||
// Close modal when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
this.closeModal(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with close button
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-close') ||
|
||||
e.target.closest('.modal-close')) {
|
||||
const modal = e.target.closest('.modal');
|
||||
if (modal) {
|
||||
this.closeModal(modal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const openModal = document.querySelector('.modal.show');
|
||||
if (openModal) {
|
||||
this.closeModal(openModal);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Focus first focusable element
|
||||
const focusableElement = modal.querySelector('input, textarea, select, button, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElement) {
|
||||
setTimeout(() => focusableElement.focus(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeModal(modal) {
|
||||
if (modal) {
|
||||
modal.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown Management
|
||||
initDropdowns() {
|
||||
const dropdowns = document.querySelectorAll('.dropdown');
|
||||
|
||||
dropdowns.forEach(dropdown => {
|
||||
const trigger = dropdown.querySelector('.dropdown-trigger');
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
|
||||
if (trigger && menu) {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleDropdown(dropdown);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
this.closeAllDropdowns();
|
||||
});
|
||||
|
||||
// Close dropdowns with Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleDropdown(dropdown) {
|
||||
const menu = dropdown.querySelector('.dropdown-menu');
|
||||
const isOpen = menu.classList.contains('show');
|
||||
|
||||
this.closeAllDropdowns();
|
||||
|
||||
if (!isOpen) {
|
||||
menu.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
closeAllDropdowns() {
|
||||
const openMenus = document.querySelectorAll('.dropdown-menu.show');
|
||||
openMenus.forEach(menu => {
|
||||
menu.classList.remove('show');
|
||||
});
|
||||
}
|
||||
|
||||
// Flash Messages
|
||||
initFlashMessages() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
|
||||
alerts.forEach(alert => {
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.dismissAlert(alert);
|
||||
}, 5000);
|
||||
|
||||
// Manual dismiss
|
||||
const closeBtn = alert.querySelector('.alert-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.dismissAlert(alert);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dismissAlert(alert) {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transform = 'translateX(100%)';
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Download QR Code functionality
|
||||
downloadQR(base64Image, filename) {
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
link.href = `data:image/png;base64,${base64Image}`;
|
||||
link.download = `${filename.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_qr_code.png`;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Show success toast
|
||||
this.showToast("QR code downloaded successfully!", "success");
|
||||
} catch (error) {
|
||||
this.showToast("Failed to download QR code", "error");
|
||||
console.error("Download error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Download from modal
|
||||
downloadModalQR() {
|
||||
if (window.currentModalQR) {
|
||||
const base64Data = window.currentModalQR.image.split("base64,")[1];
|
||||
this.downloadQR(base64Data, window.currentModalQR.name);
|
||||
}
|
||||
}
|
||||
|
||||
// QR Modal functionality
|
||||
openQRModal(qrData, qrName) {
|
||||
const modal = document.getElementById("qrModal");
|
||||
const modalTitle = document.getElementById("modalTitle");
|
||||
const modalImage = document.getElementById("modalQRImage");
|
||||
|
||||
if (modal && modalTitle && modalImage) {
|
||||
modalTitle.textContent = `${qrName} - QR Code`;
|
||||
modalImage.src = `data:image/png;base64,${qrData}`;
|
||||
modalImage.alt = `QR Code for ${qrName}`;
|
||||
|
||||
// Store current modal QR for download
|
||||
window.currentModalQR = {
|
||||
name: qrName,
|
||||
image: `data:image/png;base64,${qrData}`,
|
||||
};
|
||||
|
||||
modal.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
closeQRModal() {
|
||||
const modal = document.getElementById("qrModal");
|
||||
if (modal) {
|
||||
modal.classList.remove('show');
|
||||
window.currentModalQR = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility Methods
|
||||
showToast(message, type = 'info', duration = 3000) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `alert alert-${type}`;
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-info-circle"></i>
|
||||
${message}
|
||||
<button class="alert-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('.flash-messages') || document.body;
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// Auto dismiss
|
||||
setTimeout(() => {
|
||||
this.dismissAlert(toast);
|
||||
}, duration);
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Form Validation Helper
|
||||
validateForm(formElement) {
|
||||
const requiredFields = formElement.querySelectorAll('[required]');
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
if (!field.value.trim()) {
|
||||
this.showFieldError(field, 'This field is required');
|
||||
isValid = false;
|
||||
} else {
|
||||
this.clearFieldError(field);
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
showFieldError(field, message) {
|
||||
this.clearFieldError(field);
|
||||
|
||||
field.classList.add('error');
|
||||
const errorElement = document.createElement('div');
|
||||
errorElement.className = 'field-error';
|
||||
errorElement.textContent = message;
|
||||
|
||||
field.parentNode.appendChild(errorElement);
|
||||
}
|
||||
|
||||
clearFieldError(field) {
|
||||
field.classList.remove('error');
|
||||
const existingError = field.parentNode.querySelector('.field-error');
|
||||
if (existingError) {
|
||||
existingError.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX Helper
|
||||
async makeRequest(url, options = {}) {
|
||||
// Read the CSRF token injected by Flask into window.qrConfig
|
||||
const csrfToken = (window.qrConfig && window.qrConfig.csrfToken) || '';
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, defaultOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error);
|
||||
this.showToast('An error occurred. Please try again.', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep existing date/time utilities
|
||||
const DateTimeUtils = {
|
||||
formatDate: (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
},
|
||||
|
||||
formatDateTime: (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
},
|
||||
|
||||
formatTime: (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Legacy Navigation Manager for backward compatibility
|
||||
class NavigationManager {
|
||||
constructor() {
|
||||
this.initMobileMenu();
|
||||
this.initDropdowns();
|
||||
}
|
||||
|
||||
initMobileMenu() {
|
||||
const mobileMenuBtn = document.getElementById("mobile-menu");
|
||||
const navMenu = document.getElementById("navMenu");
|
||||
|
||||
if (mobileMenuBtn && navMenu) {
|
||||
mobileMenuBtn.addEventListener("click", () => {
|
||||
mobileMenuBtn.classList.toggle("active");
|
||||
navMenu.classList.toggle("active");
|
||||
});
|
||||
|
||||
const navLinks = navMenu.querySelectorAll(".nav-link");
|
||||
navLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
mobileMenuBtn.classList.remove("active");
|
||||
navMenu.classList.remove("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initDropdowns() {
|
||||
const dropdowns = document.querySelectorAll(".dropdown");
|
||||
|
||||
dropdowns.forEach((dropdown) => {
|
||||
const trigger = dropdown.querySelector(".dropdown-trigger");
|
||||
const menu = dropdown.querySelector(".dropdown-menu");
|
||||
|
||||
if (trigger && menu) {
|
||||
trigger.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleDropdown(dropdown);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", () => {
|
||||
this.closeAllDropdowns();
|
||||
});
|
||||
}
|
||||
|
||||
toggleDropdown(dropdown) {
|
||||
const menu = dropdown.querySelector(".dropdown-menu");
|
||||
const isOpen = menu.classList.contains("show");
|
||||
|
||||
this.closeAllDropdowns();
|
||||
|
||||
if (!isOpen) {
|
||||
menu.classList.add("show");
|
||||
}
|
||||
}
|
||||
|
||||
closeAllDropdowns() {
|
||||
const openMenus = document.querySelectorAll(".dropdown-menu.show");
|
||||
openMenus.forEach((menu) => {
|
||||
menu.classList.remove("show");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize main app if sidebar exists (authenticated users)
|
||||
if (document.getElementById('sidebar')) {
|
||||
window.qrManager = new QRManager();
|
||||
|
||||
// Make functions globally available for inline event handlers
|
||||
window.downloadQR = (base64Image, filename) => {
|
||||
window.qrManager.downloadQR(base64Image, filename);
|
||||
};
|
||||
|
||||
window.downloadModalQR = () => {
|
||||
window.qrManager.downloadModalQR();
|
||||
};
|
||||
|
||||
window.openQRModal = (qrData, qrName) => {
|
||||
window.qrManager.openQRModal(qrData, qrName);
|
||||
};
|
||||
|
||||
window.closeQRModal = () => {
|
||||
window.qrManager.closeQRModal();
|
||||
};
|
||||
} else {
|
||||
// Initialize legacy navigation for non-authenticated pages
|
||||
window.navigationManager = new NavigationManager();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for use in other scripts
|
||||
window.DateTimeUtils = DateTimeUtils;
|
||||
|
||||
// Keep any existing global functions for backward compatibility
|
||||
if (typeof showConfirmation === 'undefined') {
|
||||
window.showConfirmation = async function(title, message, description = '') {
|
||||
return new Promise((resolve) => {
|
||||
const confirmed = confirm(`${title}\n\n${message}\n${description}`);
|
||||
resolve(confirmed);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
/**
|
||||
* Users management JavaScript functionality
|
||||
* static/js/users.js
|
||||
*/
|
||||
|
||||
class UsersManager {
|
||||
constructor() {
|
||||
this.selectedUsers = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeSearch();
|
||||
this.initializeFilters();
|
||||
this.initializeBulkActions();
|
||||
this.setupEventListeners();
|
||||
this.initializeModals();
|
||||
}
|
||||
|
||||
// Initialize search functionality
|
||||
initializeSearch() {
|
||||
const searchInput = document.getElementById("searchUsers");
|
||||
if (!searchInput) return;
|
||||
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener("input", () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.filterUsers();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize filter functionality
|
||||
initializeFilters() {
|
||||
const filters = ["roleFilter", "statusFilter"];
|
||||
|
||||
filters.forEach((filterId) => {
|
||||
const filter = document.getElementById(filterId);
|
||||
if (filter) {
|
||||
filter.addEventListener("change", () => {
|
||||
this.filterUsers();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize bulk actions
|
||||
initializeBulkActions() {
|
||||
const selectAllCheckbox = document.getElementById("selectAllUsers");
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener("change", (e) => {
|
||||
this.toggleSelectAll(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Individual checkbox handlers
|
||||
const userCheckboxes = document.querySelectorAll(".user-checkbox");
|
||||
userCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", (e) => {
|
||||
this.handleUserSelection(e.target);
|
||||
});
|
||||
});
|
||||
|
||||
// Bulk action buttons
|
||||
this.setupBulkActionButtons();
|
||||
}
|
||||
|
||||
setupBulkActionButtons() {
|
||||
const bulkDeactivateBtn = document.getElementById("bulkDeactivateBtn");
|
||||
const bulkActivateBtn = document.getElementById("bulkActivateBtn");
|
||||
const bulkDeleteBtn = document.getElementById("bulkDeleteBtn");
|
||||
|
||||
if (bulkDeactivateBtn) {
|
||||
bulkDeactivateBtn.addEventListener("click", () => {
|
||||
this.bulkDeactivateUsers();
|
||||
});
|
||||
}
|
||||
|
||||
if (bulkActivateBtn) {
|
||||
bulkActivateBtn.addEventListener("click", () => {
|
||||
this.bulkActivateUsers();
|
||||
});
|
||||
}
|
||||
|
||||
if (bulkDeleteBtn) {
|
||||
bulkDeleteBtn.addEventListener("click", () => {
|
||||
this.bulkDeleteUsers();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
setupEventListeners() {
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// ESC to close modals
|
||||
if (e.key === "Escape") {
|
||||
this.closeAllModals();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + F to focus search
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
const searchInput = document.getElementById("searchUsers");
|
||||
if (searchInput) searchInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside dropdowns to close
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest(".dropdown")) {
|
||||
this.closeAllDropdowns();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize modal functionality
|
||||
initializeModals() {
|
||||
const modals = document.querySelectorAll(".modal");
|
||||
modals.forEach((modal) => {
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeModal(modal);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Filter users based on search and filters
|
||||
filterUsers() {
|
||||
const searchTerm =
|
||||
document.getElementById("searchUsers")?.value.toLowerCase() || "";
|
||||
const roleFilter = document.getElementById("roleFilter")?.value || "";
|
||||
const statusFilter = document.getElementById("statusFilter")?.value || "";
|
||||
|
||||
const userRows = document.querySelectorAll(".user-row");
|
||||
let visibleCount = 0;
|
||||
|
||||
userRows.forEach((row) => {
|
||||
const name = row.dataset.name?.toLowerCase() || "";
|
||||
const email = row.dataset.email?.toLowerCase() || "";
|
||||
const username = row.dataset.username?.toLowerCase() || "";
|
||||
const role = row.dataset.role || "";
|
||||
const status = row.dataset.status || "";
|
||||
|
||||
const matchesSearch =
|
||||
!searchTerm ||
|
||||
name.includes(searchTerm) ||
|
||||
email.includes(searchTerm) ||
|
||||
username.includes(searchTerm);
|
||||
|
||||
const matchesRole = !roleFilter || role === roleFilter;
|
||||
const matchesStatus = !statusFilter || status === statusFilter;
|
||||
|
||||
if (matchesSearch && matchesRole && matchesStatus) {
|
||||
this.showUserRow(row);
|
||||
visibleCount++;
|
||||
} else {
|
||||
this.hideUserRow(row);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateResultsCount(visibleCount);
|
||||
}
|
||||
|
||||
showUserRow(row) {
|
||||
row.style.display = "table-row";
|
||||
row.classList.remove("fade-out");
|
||||
row.classList.add("fade-in");
|
||||
}
|
||||
|
||||
hideUserRow(row) {
|
||||
row.classList.remove("fade-in");
|
||||
row.classList.add("fade-out");
|
||||
setTimeout(() => {
|
||||
if (row.classList.contains("fade-out")) {
|
||||
row.style.display = "none";
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updateResultsCount(count) {
|
||||
const counter = document.querySelector(".results-counter");
|
||||
if (counter) {
|
||||
counter.textContent = `${count} users found`;
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown management
|
||||
toggleDropdown(event, button) {
|
||||
event.stopPropagation();
|
||||
|
||||
const dropdown = button.closest(".dropdown");
|
||||
const menu = dropdown.querySelector(".dropdown-menu");
|
||||
|
||||
// Close all other dropdowns
|
||||
this.closeAllDropdowns();
|
||||
|
||||
// Toggle current dropdown
|
||||
menu.classList.toggle("show");
|
||||
}
|
||||
|
||||
closeAllDropdowns() {
|
||||
const openMenus = document.querySelectorAll(".dropdown-menu.show");
|
||||
openMenus.forEach((menu) => {
|
||||
menu.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
// User Actions
|
||||
async deactivateUser(userId, userName) {
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Deactivate User",
|
||||
`Are you sure you want to deactivate ${userName}?`,
|
||||
"This will disable their login access but preserve their data."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/users/${userId}/delete`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update UI
|
||||
this.updateUserStatus(userId, "inactive");
|
||||
window.showToast(
|
||||
`User ${userName} deactivated successfully`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to deactivate user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Deactivation error:", error);
|
||||
window.showToast("Failed to deactivate user", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async reactivateUser(userId, userName) {
|
||||
try {
|
||||
const response = await fetch(`/users/${userId}/reactivate`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.updateUserStatus(userId, "active");
|
||||
window.showToast(
|
||||
`User ${userName} reactivated successfully`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to reactivate user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Reactivation error:", error);
|
||||
window.showToast("Failed to reactivate user", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async promoteUser(userId, userName) {
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Promote to Admin",
|
||||
`Promote ${userName} to admin?`,
|
||||
"This will give them full system access including user management and system settings."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/users/${userId}/promote`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.updateUserRole(userId, "admin");
|
||||
window.showToast(
|
||||
`${userName} promoted to admin successfully`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to promote user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Promotion error:", error);
|
||||
window.showToast("Failed to promote user", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async demoteUser(userId, userName) {
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Demote from Admin",
|
||||
`Demote ${userName} from admin to staff?`,
|
||||
"This will remove their admin privileges and limit access to QR code management only."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/users/${userId}/demote`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.updateUserRole(userId, "staff");
|
||||
window.showToast(
|
||||
`${userName} demoted to staff successfully`,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
throw new Error("Failed to demote user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Demotion error:", error);
|
||||
window.showToast("Failed to demote user", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async permanentlyDeleteUser(userId, userName) {
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Permanently Delete User",
|
||||
`⚠️ PERMANENTLY DELETE ${userName}?`,
|
||||
"This action CANNOT be undone and will permanently remove the user account and all associated QR codes.",
|
||||
"danger"
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/users/${userId}/permanently-delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Remove user row from table
|
||||
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||
if (userRow) {
|
||||
userRow.classList.add("fade-out");
|
||||
setTimeout(() => userRow.remove(), 300);
|
||||
}
|
||||
|
||||
window.showToast(`User ${userName} permanently deleted`, "success");
|
||||
} else {
|
||||
throw new Error("Failed to delete user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Deletion error:", error);
|
||||
window.showToast("Failed to delete user", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI after user actions
|
||||
updateUserStatus(userId, newStatus) {
|
||||
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||
if (!userRow) return;
|
||||
|
||||
userRow.dataset.status = newStatus;
|
||||
|
||||
const statusBadge = userRow.querySelector(".user-status");
|
||||
if (statusBadge) {
|
||||
statusBadge.className = `user-status ${newStatus}`;
|
||||
statusBadge.innerHTML = `
|
||||
<i class="fas ${
|
||||
newStatus === "active" ? "fa-check-circle" : "fa-times-circle"
|
||||
}"></i>
|
||||
${newStatus === "active" ? "Active" : "Inactive"}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update action buttons in dropdown
|
||||
this.updateUserActions(userId, newStatus);
|
||||
}
|
||||
|
||||
updateUserRole(userId, newRole) {
|
||||
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||
if (!userRow) return;
|
||||
|
||||
userRow.dataset.role = newRole;
|
||||
|
||||
const roleBadge = userRow.querySelector(".user-role");
|
||||
if (roleBadge) {
|
||||
roleBadge.className = `user-role ${newRole}`;
|
||||
roleBadge.textContent = newRole;
|
||||
}
|
||||
|
||||
// Update action buttons
|
||||
this.updateUserActions(userId, null, newRole);
|
||||
}
|
||||
|
||||
updateUserActions(userId, status = null, role = null) {
|
||||
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||
if (!userRow) return;
|
||||
|
||||
const currentStatus = status || userRow.dataset.status;
|
||||
const currentRole = role || userRow.dataset.role;
|
||||
|
||||
// Update dropdown menu items
|
||||
const dropdownMenu = userRow.querySelector(".dropdown-menu");
|
||||
if (dropdownMenu) {
|
||||
// This would update the dropdown items based on new status/role
|
||||
// Implementation depends on your dropdown structure
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk Actions
|
||||
toggleSelectAll(checked) {
|
||||
const userCheckboxes = document.querySelectorAll(".user-checkbox");
|
||||
userCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = checked;
|
||||
this.handleUserSelection(checkbox);
|
||||
});
|
||||
}
|
||||
|
||||
handleUserSelection(checkbox) {
|
||||
const userId = checkbox.value;
|
||||
|
||||
if (checkbox.checked) {
|
||||
this.selectedUsers.add(userId);
|
||||
} else {
|
||||
this.selectedUsers.delete(userId);
|
||||
}
|
||||
|
||||
this.updateBulkActionsBar();
|
||||
this.updateSelectAllState();
|
||||
}
|
||||
|
||||
updateBulkActionsBar() {
|
||||
const bulkActionsBar = document.getElementById("bulkActionsBar");
|
||||
const selectedCount = document.getElementById("selectedCount");
|
||||
|
||||
if (bulkActionsBar && selectedCount) {
|
||||
if (this.selectedUsers.size > 0) {
|
||||
bulkActionsBar.classList.add("show");
|
||||
selectedCount.textContent = this.selectedUsers.size;
|
||||
} else {
|
||||
bulkActionsBar.classList.remove("show");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectAllState() {
|
||||
const selectAllCheckbox = document.getElementById("selectAllUsers");
|
||||
const userCheckboxes = document.querySelectorAll(".user-checkbox");
|
||||
|
||||
if (selectAllCheckbox && userCheckboxes.length > 0) {
|
||||
const checkedCount = Array.from(userCheckboxes).filter(
|
||||
(cb) => cb.checked
|
||||
).length;
|
||||
selectAllCheckbox.checked = checkedCount === userCheckboxes.length;
|
||||
selectAllCheckbox.indeterminate =
|
||||
checkedCount > 0 && checkedCount < userCheckboxes.length;
|
||||
}
|
||||
}
|
||||
|
||||
async bulkDeactivateUsers() {
|
||||
if (this.selectedUsers.size === 0) return;
|
||||
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Bulk Deactivate Users",
|
||||
`Deactivate ${this.selectedUsers.size} selected users?`,
|
||||
"This will disable their login access but preserve their data."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/users/bulk/deactivate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_ids: Array.from(this.selectedUsers),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Update UI for deactivated users
|
||||
this.selectedUsers.forEach((userId) => {
|
||||
this.updateUserStatus(userId, "inactive");
|
||||
});
|
||||
|
||||
this.clearSelection();
|
||||
window.showToast(result.message, "success");
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Bulk deactivation error:", error);
|
||||
window.showToast("Failed to deactivate users", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async bulkActivateUsers() {
|
||||
if (this.selectedUsers.size === 0) return;
|
||||
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Bulk Activate Users",
|
||||
`Activate ${this.selectedUsers.size} selected users?`,
|
||||
"This will restore their login access."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/users/bulk/activate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_ids: Array.from(this.selectedUsers),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.selectedUsers.forEach((userId) => {
|
||||
this.updateUserStatus(userId, "active");
|
||||
});
|
||||
|
||||
this.clearSelection();
|
||||
window.showToast(result.message, "success");
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Bulk activation error:", error);
|
||||
window.showToast("Failed to activate users", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async bulkDeleteUsers() {
|
||||
if (this.selectedUsers.size === 0) return;
|
||||
|
||||
const confirmed = await this.showConfirmation(
|
||||
"Permanently Delete Users",
|
||||
`⚠️ PERMANENTLY DELETE ${this.selectedUsers.size} selected users?`,
|
||||
"This action CANNOT be undone and will permanently remove all user accounts and their associated data.",
|
||||
"danger"
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/users/bulk/permanently-delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
'X-CSRF-Token': (window.qrConfig && window.qrConfig.csrfToken) || '',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_ids: Array.from(this.selectedUsers),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Remove users from table
|
||||
this.selectedUsers.forEach((userId) => {
|
||||
const userRow = document.querySelector(`[data-user-id="${userId}"]`);
|
||||
if (userRow) {
|
||||
userRow.classList.add("fade-out");
|
||||
setTimeout(() => userRow.remove(), 300);
|
||||
}
|
||||
});
|
||||
|
||||
this.clearSelection();
|
||||
window.showToast(result.message, "success");
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Bulk deletion error:", error);
|
||||
window.showToast("Failed to delete users", "error");
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedUsers.clear();
|
||||
const userCheckboxes = document.querySelectorAll(".user-checkbox");
|
||||
userCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
this.updateBulkActionsBar();
|
||||
this.updateSelectAllState();
|
||||
}
|
||||
|
||||
// Modal and confirmation dialogs
|
||||
showConfirmation(title, message, details = "", type = "warning") {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "modal";
|
||||
modal.style.display = "flex";
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content confirmation-modal">
|
||||
<div class="modal-header">
|
||||
<h3>
|
||||
<i class="fas ${
|
||||
type === "danger"
|
||||
? "fa-exclamation-triangle text-danger"
|
||||
: "fa-question-circle text-warning"
|
||||
}"></i>
|
||||
${title}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><strong>${message}</strong></p>
|
||||
${details ? `<p class="text-muted">${details}</p>` : ""}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary cancel-btn">Cancel</button>
|
||||
<button class="btn btn-${
|
||||
type === "danger" ? "danger" : "warning"
|
||||
} confirm-btn">
|
||||
<i class="fas fa-check"></i> Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const cancelBtn = modal.querySelector(".cancel-btn");
|
||||
const confirmBtn = modal.querySelector(".confirm-btn");
|
||||
|
||||
const cleanup = () => modal.remove();
|
||||
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
confirmBtn.addEventListener("click", () => {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
closeAllModals() {
|
||||
const modals = document.querySelectorAll('.modal[style*="flex"]');
|
||||
modals.forEach((modal) => this.closeModal(modal));
|
||||
}
|
||||
|
||||
// User details modal
|
||||
showUserDetails(userId) {
|
||||
// Implementation for showing user details modal
|
||||
const modal = document.getElementById("userDetailsModal");
|
||||
if (modal) {
|
||||
// Populate modal with user data
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
closeUserDetailsModal() {
|
||||
const modal = document.getElementById("userDetailsModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Password reset modal
|
||||
showPasswordResetModal(userId) {
|
||||
const modal = document.getElementById("passwordResetModal");
|
||||
if (modal) {
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
closePasswordResetModal() {
|
||||
const modal = document.getElementById("passwordResetModal");
|
||||
if (modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize users manager when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
window.usersManager = new UsersManager();
|
||||
|
||||
// Global functions for inline event handlers
|
||||
window.toggleDropdown = (event, button) =>
|
||||
window.usersManager.toggleDropdown(event, button);
|
||||
window.deactivateUser = (userId, userName) =>
|
||||
window.usersManager.deactivateUser(userId, userName);
|
||||
window.reactivateUser = (userId, userName) =>
|
||||
window.usersManager.reactivateUser(userId, userName);
|
||||
window.promoteUser = (userId, userName) =>
|
||||
window.usersManager.promoteUser(userId, userName);
|
||||
window.demoteUser = (userId, userName) =>
|
||||
window.usersManager.demoteUser(userId, userName);
|
||||
window.permanentlyDeleteUser = (userId, userName) =>
|
||||
window.usersManager.permanentlyDeleteUser(userId, userName);
|
||||
window.showUserDetails = (userId) =>
|
||||
window.usersManager.showUserDetails(userId);
|
||||
window.closeUserDetailsModal = () =>
|
||||
window.usersManager.closeUserDetailsModal();
|
||||
window.showPasswordResetModal = (userId) =>
|
||||
window.usersManager.showPasswordResetModal(userId);
|
||||
window.closePasswordResetModal = () =>
|
||||
window.usersManager.closePasswordResetModal();
|
||||
});
|
||||
Reference in New Issue
Block a user