moving closer to something useful
This commit is contained in:
+174
-14
@@ -351,15 +351,92 @@ async function fetchData(endpoint) {
|
||||
}
|
||||
}
|
||||
|
||||
function populateSelect(id, items, codeKey, labelKey) {
|
||||
const sel = document.getElementById(id);
|
||||
sel.innerHTML = '<option value="">All</option>';
|
||||
function populateDropdown(containerId, items, codeKey, labelKey) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const checkboxList = container.querySelector('.checkbox-dropdown-list');
|
||||
checkboxList.innerHTML = '';
|
||||
|
||||
const selectAllLabel = document.createElement('label');
|
||||
selectAllLabel.className = 'checkbox-item select-all-wrapper';
|
||||
selectAllLabel.innerHTML = `
|
||||
<input type="checkbox" class="select-all" data-target="${containerId}" checked>
|
||||
<span>All</span>
|
||||
`;
|
||||
checkboxList.appendChild(selectAllLabel);
|
||||
|
||||
const separator = document.createElement('div');
|
||||
separator.className = 'dropdown-separator';
|
||||
checkboxList.appendChild(separator);
|
||||
|
||||
items.forEach((item) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = item[codeKey];
|
||||
opt.textContent = item[labelKey];
|
||||
sel.appendChild(opt);
|
||||
const label = document.createElement('label');
|
||||
label.className = 'checkbox-item';
|
||||
label.innerHTML = `
|
||||
<input type="checkbox"
|
||||
name="${containerId}"
|
||||
value="${item[codeKey]}"
|
||||
data-label="${item[labelKey]}">
|
||||
<span>${item[labelKey]}</span>
|
||||
`;
|
||||
checkboxList.appendChild(label);
|
||||
});
|
||||
|
||||
const selectAllCb = selectAllLabel.querySelector('.select-all');
|
||||
selectAllCb.addEventListener('change', (e) => {
|
||||
const cbs = checkboxList.querySelectorAll(`input[name="${containerId}"]`);
|
||||
cbs.forEach(cb => {
|
||||
if (!cb.disabled) {
|
||||
cb.checked = e.target.checked;
|
||||
}
|
||||
});
|
||||
updateDropdownLabel(containerId);
|
||||
});
|
||||
|
||||
checkboxList.querySelectorAll(`input[name="${containerId}"]`).forEach(cb => {
|
||||
cb.addEventListener('change', () => updateDropdownLabel(containerId));
|
||||
});
|
||||
|
||||
updateDropdownLabel(containerId);
|
||||
}
|
||||
|
||||
function updateDropdownLabel(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const cbs = container.querySelectorAll(`input[name="${containerId}"]`);
|
||||
const checked = Array.from(cbs).filter(cb => cb.checked && !cb.disabled);
|
||||
const labelEl = container.querySelector('.dropdown-label');
|
||||
|
||||
if (checked.length === 0) {
|
||||
labelEl.textContent = 'None selected';
|
||||
} else if (checked.length === cbs.length) {
|
||||
labelEl.textContent = 'All selected';
|
||||
} else {
|
||||
labelEl.textContent = `${checked.length} selected`;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDropdown(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const list = container.querySelector('.checkbox-dropdown-list');
|
||||
const isOpen = list.classList.contains('open');
|
||||
|
||||
document.querySelectorAll('.checkbox-dropdown-list.open').forEach(el => {
|
||||
el.classList.remove('open');
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
list.classList.add('open');
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedValues(groupName) {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${groupName}"]:checked`);
|
||||
return Array.from(checkboxes).filter(cb => !cb.disabled).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function populateMonthSelect(selectId, months) {
|
||||
@@ -383,15 +460,15 @@ async function populateDropdowns() {
|
||||
]);
|
||||
|
||||
if (speciesList) {
|
||||
populateSelect('species', speciesList, 'code', 'label');
|
||||
populateDropdown('species', speciesList, 'code', 'label');
|
||||
}
|
||||
|
||||
if (zonesList) {
|
||||
populateSelect('zone', zonesList, 'code', 'label');
|
||||
populateDropdown('zone', zonesList, 'code', 'label');
|
||||
}
|
||||
|
||||
if (gearList) {
|
||||
populateSelect('gear', gearList, 'code', 'label');
|
||||
populateDropdown('gear', gearList, 'code', 'label');
|
||||
}
|
||||
|
||||
if (summaryData && summaryData.monthly) {
|
||||
@@ -405,16 +482,62 @@ async function populateDropdowns() {
|
||||
document.getElementById('month_to').value = allMonths[allMonths.length - 1];
|
||||
}
|
||||
|
||||
await updateAvailableFilters();
|
||||
|
||||
showLoading(false);
|
||||
}
|
||||
|
||||
async function updateAvailableFilters() {
|
||||
const species = getSelectedValues('species');
|
||||
const zone = getSelectedValues('zone');
|
||||
const gear = getSelectedValues('gear');
|
||||
const monthFrom = document.getElementById('month_from').value;
|
||||
const monthTo = document.getElementById('month_to').value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (species.length > 0) params.append('species_multi', species.join(','));
|
||||
if (zone.length > 0) params.append('zone_multi', zone.join(','));
|
||||
if (gear.length > 0) params.append('gear_multi', gear.join(','));
|
||||
if (monthFrom) params.append('month_from', monthFrom);
|
||||
if (monthTo) params.append('month_to', monthTo);
|
||||
|
||||
const available = await fetchData(`/api/available-filters?${params.toString()}`);
|
||||
|
||||
if (available) {
|
||||
disableUnavailableOptions('species', available.species);
|
||||
disableUnavailableOptions('zone', available.zones);
|
||||
disableUnavailableOptions('gear', available.gear);
|
||||
}
|
||||
}
|
||||
|
||||
function disableUnavailableOptions(containerId, availableCodes) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
const checkboxes = container.querySelectorAll(`input[type="checkbox"]`);
|
||||
checkboxes.forEach(cb => {
|
||||
if (cb.classList.contains('select-all')) return;
|
||||
|
||||
const isAvailable = availableCodes.includes(cb.value);
|
||||
cb.disabled = !isAvailable;
|
||||
cb.parentElement.style.opacity = isAvailable ? '1' : '0.4';
|
||||
cb.parentElement.style.cursor = isAvailable ? 'pointer' : 'not-allowed';
|
||||
|
||||
if (!isAvailable && cb.checked) {
|
||||
cb.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
updateDropdownLabel(containerId);
|
||||
}
|
||||
|
||||
async function updateCharts() {
|
||||
showLoading(true);
|
||||
hideEmptyState();
|
||||
|
||||
const species = document.getElementById('species').value;
|
||||
const zone = document.getElementById('zone').value;
|
||||
const gear = document.getElementById('gear').value;
|
||||
const species = getSelectedValues('species');
|
||||
const zone = getSelectedValues('zone');
|
||||
const gear = getSelectedValues('gear');
|
||||
const monthFrom = document.getElementById('month_from').value;
|
||||
const monthTo = document.getElementById('month_to').value;
|
||||
|
||||
@@ -425,7 +548,9 @@ async function updateCharts() {
|
||||
}
|
||||
|
||||
const summaryParams = new URLSearchParams();
|
||||
if (species) summaryParams.append('species', species);
|
||||
if (species.length > 0) summaryParams.append('species_multi', species.join(','));
|
||||
if (zone.length > 0) summaryParams.append('zone_multi', zone.join(','));
|
||||
if (gear.length > 0) summaryParams.append('gear_multi', gear.join(','));
|
||||
if (monthFrom) summaryParams.append('month_from', monthFrom);
|
||||
if (monthTo) summaryParams.append('month_to', monthTo);
|
||||
|
||||
@@ -526,6 +651,15 @@ function hideEmptyState() {
|
||||
|
||||
document.getElementById('apply-filters').addEventListener('click', updateCharts);
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.filter-group.multi-select')) {
|
||||
document.querySelectorAll('.checkbox-dropdown-list.open').forEach(el => {
|
||||
el.classList.remove('open');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(() => {
|
||||
function currentSystemTheme() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
@@ -558,5 +692,31 @@ document.getElementById('apply-filters').addEventListener('click', updateCharts)
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initCharts();
|
||||
|
||||
// Setup click handlers for dropdown toggles
|
||||
['species', 'zone', 'gear'].forEach(id => {
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
const label = container.querySelector('.dropdown-label');
|
||||
if (label) {
|
||||
label.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleDropdown(id);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for changes inside the dropdown list
|
||||
const list = container.querySelector('.checkbox-dropdown-list');
|
||||
if (list) {
|
||||
list.addEventListener('change', () => {
|
||||
updateAvailableFilters();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('month_from').addEventListener('change', updateAvailableFilters);
|
||||
document.getElementById('month_to').addEventListener('change', updateAvailableFilters);
|
||||
|
||||
populateDropdowns().then(updateCharts);
|
||||
});
|
||||
|
||||
+12
-9
@@ -36,17 +36,20 @@
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label for="species">Species</label>
|
||||
<select id="species"><option value="">All</option></select>
|
||||
<div class="filter-group multi-select" id="species">
|
||||
<label>Species</label>
|
||||
<div class="dropdown-trigger dropdown-label">All</div>
|
||||
<div class="checkbox-dropdown-list"></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="zone">Zone</label>
|
||||
<select id="zone"><option value="">All</option></select>
|
||||
<div class="filter-group multi-select" id="zone">
|
||||
<label>Zone</label>
|
||||
<div class="dropdown-trigger dropdown-label">All</div>
|
||||
<div class="checkbox-dropdown-list"></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="gear">Gear</label>
|
||||
<select id="gear"><option value="">All</option></select>
|
||||
<div class="filter-group multi-select" id="gear">
|
||||
<label>Gear</label>
|
||||
<div class="dropdown-trigger dropdown-label">All</div>
|
||||
<div class="checkbox-dropdown-list"></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="month_from">From</label>
|
||||
|
||||
+88
-2
@@ -207,6 +207,7 @@ nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
@@ -231,6 +232,91 @@ nav {
|
||||
}
|
||||
.filter-group select:focus { border-color: var(--accent); }
|
||||
|
||||
/* ---- Multi-Select Dropdown ---- */
|
||||
.filter-group.multi-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
color: var(--ink);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-trigger:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dropdown-trigger::after {
|
||||
content: '▼';
|
||||
font-size: 0.6rem;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.checkbox-dropdown-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 2px 2px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
display: none;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.checkbox-dropdown-list.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.select-all-wrapper {
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
align-self: flex-end;
|
||||
padding: 0.5rem 1.2rem;
|
||||
@@ -269,8 +355,8 @@ nav {
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.chart-container:nth-child(2), /* Donut */
|
||||
.chart-container:nth-child(3) { /* Bar */
|
||||
.chart-container:nth-child(2),
|
||||
.chart-container:nth-child(3) {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user