Compare commits
5
Commits
9814ed4d84
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80a52ed790 | ||
|
|
b299bf3627 | ||
|
|
69a9370104 | ||
|
|
95dbf16d04 | ||
|
|
978a5b64d0 |
@@ -0,0 +1,163 @@
|
||||
# Order Dashboard - Project Guide
|
||||
|
||||
## Overview
|
||||
|
||||
A professional, modern dashboard for order management that fetches data from a webhook API. Features include real-time data loading, column filtering, sorting, global search, authentication handling, and recheck status tracking.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `index.html` | Main HTML structure, links to CSS and JS |
|
||||
| `style.css` | All styling and animations |
|
||||
| `app.js` | Application logic, API calls, state management |
|
||||
|
||||
## API
|
||||
|
||||
- **Endpoint:** `https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0`
|
||||
- **GET:** Fetches order data
|
||||
- **POST:** Updates recheck status (body: `{Order_ID: string, recheck: boolean}`)
|
||||
- **Auth:** Basic Authentication (username:password) via `Authorization: Basic <base64>` header
|
||||
- **Response format:** `{"data": [{Month, Odoo_AR_Nr, Order_ID, Country_Name, VAT, Remark, recheck?}]}`
|
||||
|
||||
## Data Model
|
||||
|
||||
```javascript
|
||||
{
|
||||
Month: string,
|
||||
Odoo_AR_Nr: string,
|
||||
Order_ID: string,
|
||||
Country_Name: string | null,
|
||||
VAT: string | null,
|
||||
Remark: string | null,
|
||||
recheck: boolean | undefined
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Real-time data loading from webhook API
|
||||
- Column-level filtering with checkboxes
|
||||
- Column sorting (ascending/descending)
|
||||
- Global search across all fields
|
||||
- Statistics dashboard (total orders, countries, VAT entries)
|
||||
- Authentication modal with credential persistence
|
||||
- Recheck checkboxes with POST updates
|
||||
- NOK remark detection (red highlighting)
|
||||
- Country classification with color badges
|
||||
- Responsive design for mobile/desktop
|
||||
- Debug console logging
|
||||
|
||||
## Architecture
|
||||
|
||||
### State Management
|
||||
- `allData` - Raw data from API
|
||||
- `filteredData` - Data after global search/filtering
|
||||
- `currentSort` - Current sort column and direction
|
||||
- `filters` - Column filter selections
|
||||
- `globalSearch` - Search term
|
||||
- `apiCredentials` - Current auth session
|
||||
|
||||
### Key Functions
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `loadData()` | Fetches data from API, handles auth |
|
||||
| `applyFilters()` | Applies search and column filters |
|
||||
| `sortTable()` | Sorts data by column |
|
||||
| `renderTable()` | Generates HTML table rows |
|
||||
| `handleRecheck()` | POSTs recheck status change |
|
||||
| `showCredentialPrompt()` | Shows auth modal |
|
||||
| `toggleAuth()` | Toggle credential session |
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
### What Worked Well
|
||||
|
||||
1. **Null-safe string operations** - Always use `String(value)` or `value || ''` before calling `.includes()` or `.toString()`
|
||||
2. **Modal click handling** - Use `e.target === this` to differentiate clicking overlay vs modal children
|
||||
3. **CSS specificity** - Use `!important` strategically for modal overlay to override other styles
|
||||
4. **Separated concerns** - Keeping HTML, CSS, and JS in separate files makes debugging easier
|
||||
5. **Console debugging** - Color-coded, grouped console logs make debugging fast
|
||||
|
||||
### Critical Bug Patterns
|
||||
|
||||
1. **`.includes()` on null/undefined** - Always guard: `if (!val) return ''; val.toString().includes()`
|
||||
2. **Event bubbling on modals** - Clicking input inside overlay triggers overlay click handler
|
||||
3. **Colspan mismatches** - Loading/empty state must match table column count
|
||||
4. **CSS order matters** - Modal reset styles must come before modal component styles
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- Don't forget to handle `null` and `undefined` for API fields
|
||||
- Don't use `item[column] === null ? 'null' : item[column]` inconsistently (string 'null' vs actual null)
|
||||
- Don't nest modal HTML inside overlay div if overlay click handler checks `e.target === this`
|
||||
- Don't forget to escape Order_ID in onclick handlers (use single quotes)
|
||||
|
||||
## Code Style
|
||||
|
||||
- No frameworks, vanilla JS only
|
||||
- CSS custom properties for theming
|
||||
- CSS Grid for stats, Flexbox for layout
|
||||
- Async/await for API calls
|
||||
- IIFE-free, module-style globals
|
||||
- JSDoc-style comments for functions
|
||||
|
||||
## Adding New Columns
|
||||
|
||||
1. Add `<th>` in table header with filter/sort indicators
|
||||
2. Add column to filter data in `applyFilters()`
|
||||
3. Add rendering in `renderTable()`
|
||||
4. Add null-safe getter function if needed
|
||||
5. Add CSS classes if special styling required
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. API returns 401 → Show credential modal
|
||||
2. User enters credentials → Save to localStorage
|
||||
3. Next `loadData()` call → Include Basic Auth header
|
||||
4. User clicks auth button when logged in → Clear credentials
|
||||
|
||||
## Debug Console
|
||||
|
||||
All major operations log to console with colored, grouped messages:
|
||||
- Webhook Call (green header)
|
||||
- Credentials (purple header)
|
||||
- Filter State (purple header)
|
||||
- Table View (cyan header)
|
||||
- Recheck (green header)
|
||||
- Errors (red header)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Export to CSV
|
||||
- Pagination for large datasets
|
||||
- Column width resizing
|
||||
- Saved filter presets
|
||||
- Dark/light theme toggle
|
||||
- Data caching
|
||||
- Offline mode
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
# Start with live-reload
|
||||
./start.sh
|
||||
|
||||
# Or open directly
|
||||
open index.html
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Requires ES6+ features
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- All filtering/sorting is client-side
|
||||
- Consider pagination for >1000 records
|
||||
- Debounce search input for large datasets
|
||||
- Filter dropdowns are created on-demand
|
||||
@@ -0,0 +1,658 @@
|
||||
// ===================================
|
||||
// Order Dashboard - Main JavaScript
|
||||
// ===================================
|
||||
|
||||
// Global state
|
||||
let allData = [];
|
||||
let filteredData = [];
|
||||
let currentSort = { column: null, direction: null };
|
||||
let filters = {};
|
||||
let columnTextFilters = {};
|
||||
let hideOkFilter = false;
|
||||
let apiCredentials = null;
|
||||
|
||||
// API Configuration
|
||||
const API_URL = 'https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0';
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateAuthUI();
|
||||
loadData();
|
||||
setupClickOutside();
|
||||
});
|
||||
|
||||
// ===================================
|
||||
// Authentication
|
||||
// ===================================
|
||||
|
||||
function updateAuthUI() {
|
||||
const btnText = document.getElementById('authBtnText');
|
||||
if (!btnText) return;
|
||||
|
||||
const storedUsername = localStorage.getItem('api_username');
|
||||
|
||||
if (storedUsername) {
|
||||
btnText.textContent = `Logged in as ${storedUsername}`;
|
||||
} else {
|
||||
btnText.textContent = 'Enter Credentials';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAuth() {
|
||||
const storedUsername = localStorage.getItem('api_username');
|
||||
|
||||
if (storedUsername) {
|
||||
localStorage.removeItem('api_username');
|
||||
localStorage.removeItem('api_password');
|
||||
apiCredentials = null;
|
||||
updateAuthUI();
|
||||
loadData();
|
||||
} else {
|
||||
showCredentialPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Data Loading
|
||||
// ===================================
|
||||
|
||||
async function loadData() {
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading data...</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
try {
|
||||
console.group('%c Webhook Call', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;');
|
||||
console.log('%c API URL:', 'font-weight: bold; color: #6366f1;', API_URL);
|
||||
|
||||
const storedUsername = localStorage.getItem('api_username');
|
||||
const storedPassword = localStorage.getItem('api_password');
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
const fetchOptions = {};
|
||||
if (storedUsername && storedPassword) {
|
||||
apiCredentials = { username: storedUsername, password: storedPassword };
|
||||
const credentials = btoa(`${storedUsername}:${storedPassword}`);
|
||||
fetchOptions.headers = {
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
console.log('%c Using Basic Auth', 'color: #10b981;', `User: ${storedUsername}`);
|
||||
}
|
||||
|
||||
const response = await fetch(API_URL, fetchOptions);
|
||||
const duration = performance.now() - startTime;
|
||||
|
||||
console.log('%c Response Status:', response.ok ? 'color: #10b981' : 'color: #ef4444', response.status, response.statusText);
|
||||
console.log('%c Response Time:', 'font-weight: bold;', `${duration.toFixed(0)}ms`);
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authorization required');
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('%c Response Data:', 'font-weight: bold;', result);
|
||||
|
||||
if (result.data) {
|
||||
allData = result.data;
|
||||
console.log('%c Data loaded:', 'color: #10b981; font-weight: bold;', allData.length, 'records');
|
||||
console.log('%c First record:', 'color: #6366f1;', allData[0]);
|
||||
} else {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
} else {
|
||||
throw new Error('API error');
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} catch (error) {
|
||||
console.error('%c Fetch Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', error.message);
|
||||
|
||||
if (error.message === 'Authorization required') {
|
||||
showCredentialPrompt();
|
||||
}
|
||||
|
||||
console.log('Using sample data:', error.message);
|
||||
}
|
||||
|
||||
filteredData = [...allData];
|
||||
|
||||
console.group('%c Filter State', 'background: #8b5cf6; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;');
|
||||
console.log('%c Current filters:', 'font-weight: bold;', filters);
|
||||
console.log('%c Total records after filter:', 'font-weight: bold;', filteredData.length);
|
||||
console.groupEnd();
|
||||
|
||||
applyFilters();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Filtering & Sorting
|
||||
// ===================================
|
||||
|
||||
function applyFilters() {
|
||||
let result = [...filteredData];
|
||||
|
||||
// Apply column text filters
|
||||
Object.entries(columnTextFilters).forEach(([column, searchText]) => {
|
||||
if (searchText && searchText.trim()) {
|
||||
const searchLower = searchText.toLowerCase().trim();
|
||||
result = result.filter(item => {
|
||||
const value = item[column];
|
||||
return value !== null && value !== undefined &&
|
||||
String(value).toLowerCase().includes(searchLower);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Apply checkbox filters
|
||||
Object.entries(filters).forEach(([column, selectedValues]) => {
|
||||
if (selectedValues.length > 0) {
|
||||
result = result.filter(item =>
|
||||
selectedValues.includes(item[column] === null ? 'null' : item[column])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply "hide OK" filter
|
||||
if (hideOkFilter) {
|
||||
result = result.filter(item => {
|
||||
const remark = item.Remark || '';
|
||||
return !String(remark).trim().toLowerCase().includes('ok');
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(result);
|
||||
updateStats(result.length);
|
||||
|
||||
console.group('%c Table View', 'background: #06b6d4; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;');
|
||||
console.log('%c Rendering', 'font-weight: bold;', result.length, 'records');
|
||||
console.log('%c Active text filters:', Object.entries(columnTextFilters)
|
||||
.filter(([_, v]) => v && v.trim())
|
||||
.map(([k, v]) => `${k}: "${v}"`), 'color: #64748b;');
|
||||
if (hideOkFilter) {
|
||||
console.log('%c Filter:', 'color: #10b981;', 'Hide OK remarks is ON');
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function sortTable(column) {
|
||||
const direction = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
filteredData.sort((a, b) => {
|
||||
let valA = a[column];
|
||||
let valB = b[column];
|
||||
|
||||
if (valA === null) valA = '';
|
||||
if (valB === null) valB = '';
|
||||
|
||||
if (typeof valA === 'string' && typeof valB === 'string') {
|
||||
valA = valA.toLowerCase();
|
||||
valB = valB.toLowerCase();
|
||||
}
|
||||
|
||||
if (valA < valB) return direction === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentSort = { column, direction };
|
||||
renderTable(filteredData);
|
||||
updateSortIndicators(column, direction);
|
||||
}
|
||||
|
||||
function updateSortIndicators(column, direction) {
|
||||
document.querySelectorAll('.sort-indicator').forEach(indicator => {
|
||||
indicator.classList.remove('active');
|
||||
});
|
||||
|
||||
const th = Array.from(document.querySelectorAll('th')).find(
|
||||
th => th.textContent.includes(column)
|
||||
);
|
||||
if (th) {
|
||||
const indicator = th.querySelector('.sort-indicator');
|
||||
if (indicator) {
|
||||
indicator.classList.add('active');
|
||||
indicator.innerHTML = direction === 'asc'
|
||||
? '<i class="fas fa-arrow-up"></i><i class="fas fa-arrow-down"></i>'
|
||||
: '<i class="fas fa-arrow-down"></i><i class="fas fa-arrow-up"></i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Table Rendering
|
||||
// ===================================
|
||||
|
||||
function renderTable(data) {
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
if (!tableBody) return;
|
||||
|
||||
if (data.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="empty-state">
|
||||
<i class="fas fa-search"></i>
|
||||
<p>No orders found matching your filters</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = data.map(item => `
|
||||
<tr>
|
||||
<td>${item.Month || ''}</td>
|
||||
<td>${item.Odoo_AR_Nr || ''}</td>
|
||||
<td>${item.Order_ID || ''}</td>
|
||||
<td>
|
||||
<div class="cell-country">
|
||||
<span class="country-badge ${getCountryClass(item.Country_Name)}"></span>
|
||||
${item.Country_Name || 'N/A'}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cell-vat ${!item.VAT ? 'empty' : ''}">
|
||||
${item.VAT || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-remark ${getRemarkClass(item.Remark)}">
|
||||
<i class="fas ${getRemarkIcon(item.Remark)}"></i>
|
||||
${item.Remark || 'N/A'}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="recheck-checkbox"
|
||||
${item.recheck ? 'checked' : ''}
|
||||
onchange="handleRecheck('${item.Order_ID}', this.checked)">
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function getCountryClass(country) {
|
||||
if (country === null || country === undefined) return 'unknown';
|
||||
const str = String(country);
|
||||
if (str.includes('Finnland')) return 'finland';
|
||||
if (str.includes('Spanien')) return 'spanien';
|
||||
if (str.includes('Deutschland')) return 'deutschland';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function getRemarkClass(remark) {
|
||||
if (remark === null || remark === undefined) return '';
|
||||
const str = String(remark);
|
||||
if (str.includes('NOK')) return 'remark-nok';
|
||||
if (str.includes('found')) return 'remark-danger';
|
||||
if (str.includes('valid') || str.includes('OK')) return 'remark-success';
|
||||
return '';
|
||||
}
|
||||
|
||||
function getRemarkIcon(remark) {
|
||||
if (remark === null || remark === undefined) return 'fa-file-alt';
|
||||
const str = String(remark);
|
||||
if (str.includes('NOK')) return 'fa-times-circle';
|
||||
if (str.includes('found')) return 'fa-exclamation-triangle';
|
||||
if (str.includes('valid') || str.includes('OK')) return 'fa-check-circle';
|
||||
return 'fa-file-alt';
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Recheck Action
|
||||
// ===================================
|
||||
|
||||
async function handleRecheck(id, isChecked) {
|
||||
console.group('%c Recheck', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;');
|
||||
console.log('%c ID:', 'font-weight: bold;', id);
|
||||
console.log('%c Status:', isChecked ? 'color: #10b981' : 'color: #94a3b8', isChecked ? 'Checked' : 'Unchecked');
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(apiCredentials ? {
|
||||
'Authorization': `Basic ${btoa(`${apiCredentials.username}:${apiCredentials.password}`)}`
|
||||
} : {})
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ID: id,
|
||||
recheck: isChecked
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('%c Success:', 'color: #10b981; font-weight: bold;', 'Recheck status updated');
|
||||
} else {
|
||||
console.error('%c Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', response.status, response.statusText);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} catch (error) {
|
||||
console.error('%c Request Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', error.message);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Column Filters
|
||||
// ===================================
|
||||
|
||||
function toggleFilter(column) {
|
||||
const container = document.getElementById('filterContainer');
|
||||
|
||||
document.querySelectorAll('.filter-dropdown').forEach(dropdown => {
|
||||
if (dropdown.dataset.column !== column) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
let dropdown = document.querySelector(`.filter-dropdown[data-column="${column}"]`);
|
||||
|
||||
if (!dropdown) {
|
||||
dropdown = createFilterDropdown(column);
|
||||
container.appendChild(dropdown);
|
||||
}
|
||||
|
||||
if (dropdown.classList.contains('show')) {
|
||||
dropdown.classList.remove('show');
|
||||
} else {
|
||||
dropdown.classList.add('show');
|
||||
positionDropdown(dropdown, column);
|
||||
}
|
||||
}
|
||||
|
||||
function createFilterDropdown(column) {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'filter-dropdown';
|
||||
dropdown.dataset.column = column;
|
||||
|
||||
const uniqueValues = [...new Set(
|
||||
allData.map(item => item[column] === null ? 'null' : item[column])
|
||||
)];
|
||||
|
||||
dropdown.innerHTML = `
|
||||
<div class="filter-header">
|
||||
<h4>Filter ${column}</h4>
|
||||
<button class="close-btn" onclick="this.closest('.filter-dropdown').classList.remove('show')">×</button>
|
||||
</div>
|
||||
<div class="filter-list">
|
||||
${uniqueValues.map(value => `
|
||||
<label class="filter-item">
|
||||
<input type="checkbox"
|
||||
data-column="${column}"
|
||||
value="${value}"
|
||||
${filters[column] && filters[column].includes(value) ? 'checked' : ''}>
|
||||
<span>${value === 'null' ? 'No Value' : value}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="filter-clear" onclick="clearFilter('${column}')">
|
||||
Clear Filter
|
||||
</button>
|
||||
`;
|
||||
|
||||
dropdown.addEventListener('change', (e) => {
|
||||
if (e.target.type === 'checkbox') {
|
||||
updateFilter(column, e.target.value, e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
function positionDropdown(dropdown, column) {
|
||||
const th = Array.from(document.querySelectorAll('th')).find(
|
||||
th => th.textContent.includes(column)
|
||||
);
|
||||
if (th) {
|
||||
const rect = th.getBoundingClientRect();
|
||||
const tableRect = document.querySelector('table').getBoundingClientRect();
|
||||
dropdown.style.left = `${rect.left - tableRect.left}px`;
|
||||
dropdown.style.width = `${rect.width}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilter(column, value, isChecked) {
|
||||
if (!filters[column]) {
|
||||
filters[column] = [];
|
||||
}
|
||||
|
||||
const valueStr = value === 'null' ? 'null' : value;
|
||||
|
||||
if (isChecked) {
|
||||
if (!filters[column].includes(valueStr)) {
|
||||
filters[column].push(valueStr);
|
||||
}
|
||||
} else {
|
||||
filters[column] = filters[column].filter(v => v !== valueStr);
|
||||
}
|
||||
|
||||
updateFilterIndicators(column);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateFilterIndicators(column) {
|
||||
const th = Array.from(document.querySelectorAll('th')).find(
|
||||
th => th.textContent.includes(column)
|
||||
);
|
||||
if (th) {
|
||||
const indicator = th.querySelector('.filter-indicator');
|
||||
if (indicator) {
|
||||
if (filters[column] && filters[column].length > 0) {
|
||||
indicator.classList.add('active');
|
||||
} else {
|
||||
indicator.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilter(column) {
|
||||
if (filters[column]) {
|
||||
filters[column] = [];
|
||||
}
|
||||
document.querySelectorAll(`.filter-dropdown[data-column="${column}"] input[type="checkbox"]`)
|
||||
.forEach(checkbox => checkbox.checked = false);
|
||||
|
||||
updateFilterIndicators(column);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function setupClickOutside() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.filter-indicator') &&
|
||||
!e.target.closest('.filter-dropdown')) {
|
||||
document.querySelectorAll('.filter-dropdown').forEach(dropdown => {
|
||||
dropdown.classList.remove('show');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleColumnFilter(input) {
|
||||
const column = input.dataset.column;
|
||||
const value = input.value;
|
||||
|
||||
if (value) {
|
||||
columnTextFilters[column] = value;
|
||||
input.parentElement.classList.add('filter-active');
|
||||
} else {
|
||||
delete columnTextFilters[column];
|
||||
input.parentElement.classList.remove('filter-active');
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
// Clear column text filters
|
||||
columnTextFilters = {};
|
||||
|
||||
// Clear checkbox filters
|
||||
filters = {};
|
||||
|
||||
// Reset hide OK filter
|
||||
hideOkFilter = false;
|
||||
updateHideOkButton();
|
||||
|
||||
// Clear all input fields
|
||||
document.querySelectorAll('.column-filter').forEach(input => {
|
||||
input.value = '';
|
||||
input.parentElement.classList.remove('filter-active');
|
||||
});
|
||||
|
||||
// Reset filter indicators
|
||||
document.querySelectorAll('.filter-indicator').forEach(indicator => {
|
||||
indicator.classList.remove('active');
|
||||
});
|
||||
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function toggleHideOkFilter() {
|
||||
hideOkFilter = !hideOkFilter;
|
||||
updateHideOkButton();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateHideOkButton() {
|
||||
const btn = document.getElementById('hideOkBtn');
|
||||
if (!btn) return;
|
||||
|
||||
if (hideOkFilter) {
|
||||
btn.style.boxShadow = '0 0 0 2px var(--danger)';
|
||||
} else {
|
||||
btn.style.boxShadow = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Statistics
|
||||
// ===================================
|
||||
|
||||
function updateStats(count = filteredData.length) {
|
||||
const totalOrders = document.getElementById('totalOrders');
|
||||
const totalCountries = document.getElementById('totalCountries');
|
||||
const totalVAT = document.getElementById('totalVAT');
|
||||
const tableCount = document.getElementById('tableCount');
|
||||
|
||||
if (totalOrders) totalOrders.textContent = allData.length;
|
||||
if (totalCountries) totalCountries.textContent =
|
||||
[...new Set(allData.map(item => item.Country_Name))].length;
|
||||
if (totalVAT) totalVAT.textContent =
|
||||
allData.filter(item => item.VAT !== null).length;
|
||||
if (tableCount) tableCount.textContent = `${count} order${count !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
// Credential Prompt
|
||||
// ===================================
|
||||
|
||||
function showCredentialPrompt() {
|
||||
const existing = document.getElementById('credentialPrompt');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const prompt = document.createElement('div');
|
||||
prompt.id = 'credentialPrompt';
|
||||
prompt.innerHTML = `
|
||||
<div class="credential-overlay">
|
||||
<div class="credential-modal">
|
||||
<div class="credential-content">
|
||||
<div class="credential-header">
|
||||
<i class="fas fa-key"></i>
|
||||
<h2>API Credentials Required</h2>
|
||||
</div>
|
||||
<p>The webhook API requires authentication. Please enter your credentials below:</p>
|
||||
<form id="credentialForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<div class="input-group">
|
||||
<i class="fas fa-user"></i>
|
||||
<input type="text" id="username" placeholder="Enter username" autocomplete="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password/API Key</label>
|
||||
<div class="input-group">
|
||||
<i class="fas fa-lock"></i>
|
||||
<input type="password" id="password" placeholder="Enter password or API key" autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-secondary" id="cancelAuthBtn">Cancel</button>
|
||||
<button type="submit" class="btn-primary">Connect</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="credential-help">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>Your credentials will be used for this session only. They are not stored permanently.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(prompt);
|
||||
|
||||
const form = document.getElementById('credentialForm');
|
||||
form.addEventListener('submit', handleCredentials);
|
||||
|
||||
const cancelBtn = document.getElementById('cancelAuthBtn');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', closeCredentialPrompt);
|
||||
}
|
||||
|
||||
const overlay = document.querySelector('#credentialPrompt .credential-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
closeCredentialPrompt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeCredentialPrompt() {
|
||||
const prompt = document.getElementById('credentialPrompt');
|
||||
if (prompt) {
|
||||
document.body.removeChild(prompt);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCredentials(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
console.group('%c Credentials', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;');
|
||||
console.log('%c Username:', 'font-weight: bold;', username);
|
||||
console.log('%c Password length:', 'font-weight: bold;', password.length, 'characters');
|
||||
console.groupEnd();
|
||||
|
||||
localStorage.setItem('api_username', username);
|
||||
localStorage.setItem('api_password', password);
|
||||
apiCredentials = { username, password };
|
||||
|
||||
console.log('%c Credentials saved:', 'color: #10b981; font-weight: bold;', {
|
||||
username,
|
||||
passwordLength: password.length,
|
||||
authHeader: `Basic ${btoa(`${username}:${password}`)}`
|
||||
});
|
||||
|
||||
closeCredentialPrompt();
|
||||
console.log('%c Retrying connection with credentials...', 'color: #10b981; font-weight: bold;');
|
||||
loadData();
|
||||
}
|
||||
+110
-1337
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,748 @@
|
||||
/* ===================================
|
||||
Order Dashboard - Stylesheet
|
||||
=================================== */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #8b5cf6;
|
||||
--dark: #0f172a;
|
||||
--light: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--hover: #f1f5f9;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--dark);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Credential Modal Reset */
|
||||
#credentialPrompt {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
#credentialPrompt .credential-overlay,
|
||||
#credentialPrompt .credential-modal {
|
||||
position: static !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
h1 i {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-card i {
|
||||
color: var(--primary);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.search-box i {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--light);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.table-info span {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--light);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
border-bottom: 2px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--light);
|
||||
z-index: 10;
|
||||
min-width: 130px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
th .th-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-wrap: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
th .th-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark);
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.column-filter {
|
||||
width: 90px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.column-filter:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.column-filter::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
th.filter-active .column-filter {
|
||||
border-color: var(--primary);
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.th-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
th .sort-indicator {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
th .sort-indicator:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
th .sort-indicator i {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
th .sort-indicator.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
th .sort-indicator.active i:first-child {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
th .sort-indicator.active i:last-child {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
th .filter-indicator {
|
||||
display: inline-flex;
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
th .filter-indicator i {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
th .filter-indicator.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.9rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
|
||||
.cell-country {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.country-badge {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.country-badge.finland {
|
||||
background: #003580;
|
||||
}
|
||||
|
||||
.country-badge.spanien {
|
||||
background: #aa151b;
|
||||
}
|
||||
|
||||
.country-badge.deutschland {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.country-badge.unknown {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.country-badge.b2b {
|
||||
background: #00008b;
|
||||
}
|
||||
|
||||
.cell-vat {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.cell-vat.empty {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cell-remark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cell-remark.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.cell-remark.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.cell-remark.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.cell-remark.remark-nok {
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cell-remark.remark-danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.cell-remark.remark-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.recheck-checkbox {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.recheck-checkbox:checked {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.recheck-checkbox:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Filter Dropdown */
|
||||
.filter-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
z-index: 100;
|
||||
min-width: 200px;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-header h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.filter-header .close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.filter-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-item:hover {
|
||||
background: var(--hover);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-item input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-item label {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--hover);
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 3px solid rgba(99, 102, 241, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--primary);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Credential Modal Styles */
|
||||
.credential-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
z-index: 9998;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.credential-modal {
|
||||
position: static;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
width: 90%;
|
||||
max-width: 450px;
|
||||
padding: 2rem;
|
||||
animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
z-index: 9999;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@keyframes modalPop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.credential-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.credential-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.credential-header i {
|
||||
color: var(--primary);
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.credential-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--dark);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.credential-modal p {
|
||||
color: #64748b;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input-group i {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem 0.875rem 2.75rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--hover);
|
||||
color: #64748b;
|
||||
border: none;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.credential-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--hover);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.credential-help i {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.credential-modal {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user