brand filter (backend+frontend), compact checkbox grid, two-line counter

This commit is contained in:
aaverbitskiy 2026-07-18 11:15:43 +00:00
parent b393562c3a
commit 5db77b7a06
6 changed files with 44 additions and 11 deletions

View File

@ -29,6 +29,17 @@
</div>
</div>
<div class="field">
<label>Бренд</label>
<div class="dropdown" id="brand-dropdown">
<button type="button" class="dropdown-btn" id="brand-btn">
<span id="brand-btn-text">Все бренды</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="brand-panel"></div>
</div>
</div>
<div class="field">
<label>Размер</label>
<div class="dropdown" id="dimension-dropdown">
@ -47,15 +58,15 @@
</div>
<!-- Group 3: checkboxes -->
<div class="hgroup">
<div class="hgroup hgroup-checks">
<div class="field checkbox-field">
<label for="show-brand"><input type="checkbox" id="show-brand" checked /> Бренд</label>
</div>
<div class="field checkbox-field">
<label for="show-company"><input type="checkbox" id="show-company" /> Компания</label>
<label for="show-collisions"><input type="checkbox" id="show-collisions" checked /> Коллизии размещения</label>
</div>
<div class="field checkbox-field">
<label for="show-collisions"><input type="checkbox" id="show-collisions" checked /> Коллизии размещения</label>
<label for="show-company"><input type="checkbox" id="show-company" /> Компания</label>
</div>
</div>

View File

@ -6,6 +6,7 @@ function buildQuery(f: Filters): string {
const params = new URLSearchParams({
city: f.cities.join(','),
dimension: f.dimensions.join(','),
brand: f.brands.join(','),
search: f.search,
});
return params.toString();

View File

@ -28,6 +28,7 @@ const appearance = loadAppearance();
applyCssVars(appearance);
const cityDropdown = createDropdown('city');
const brandDropdown = createDropdown('brand');
const dimensionDropdown = createDropdown('dimension');
const view = createTimelineView(
{
@ -55,6 +56,7 @@ function currentFilters(): Filters {
return {
cities: cityDropdown.getSelected(),
dimensions: dimensionDropdown.getSelected(),
brands: brandDropdown.getSelected(),
search: el.search.value.trim(),
};
}
@ -85,6 +87,7 @@ document.addEventListener('click', (e) => {
async function loadMeta(): Promise<void> {
const meta = await api.meta();
cityDropdown.setValues(meta.cities, 'Все города');
brandDropdown.setValues(meta.brands, 'Все бренды');
dimensionDropdown.setValues(meta.dimensions, 'Все размеры');
}
@ -95,7 +98,7 @@ async function loadData(fit: boolean): Promise<void> {
lastBoards = boards;
lastBookings = bookings;
view.render(boards, bookings, fit, flags());
el.status.textContent = `Бордов: ${boards.length}, бронирований: ${bookings.length}`;
el.status.innerHTML = `Бордов: ${boards.length}<br>Бронирований: ${bookings.length}`;
rebuildDecorMenu(); // status list may have changed
}
@ -111,6 +114,7 @@ for (const cb of [el.showBrand, el.showCompany, el.showCollisions]) {
cb.addEventListener('change', () => view.render(lastBoards, lastBookings, false, flags()));
}
cityDropdown.onChange(scheduleReload);
brandDropdown.onChange(scheduleReload);
dimensionDropdown.onChange(scheduleReload);
el.search.addEventListener('input', scheduleReload);

View File

@ -45,6 +45,14 @@ header {
border-radius: 8px;
}
.hgroup-brand { background: #eaeaea; }
/* Compact 2-column layout for the checkbox group. */
.hgroup-checks {
display: grid;
grid-template-columns: auto auto;
column-gap: 18px;
row-gap: 4px;
align-content: center;
}
.brand { display: flex; flex-direction: column; justify-content: center; line-height: 1.2; }
.brand-name { font-size: 18px; font-weight: 700; white-space: nowrap; }
@ -59,7 +67,7 @@ input[type=text] {
font-size: 13px;
min-width: 180px;
}
#status { font-size: 12px; color: #888; margin-left: auto; align-self: center; }
#status { font-size: 12px; color: #888; margin-left: auto; align-self: center; line-height: 1.4; text-align: right; }
.checkbox-field { flex-direction: row; align-items: center; gap: 6px; align-self: center; }
.checkbox-field label {

View File

@ -6,6 +6,7 @@
export interface Meta {
cities: string[];
dimensions: string[];
brands: string[];
}
/** One row of GET /api/boards (grouped per board_id). */
@ -31,6 +32,7 @@ export interface Booking {
export interface Filters {
cities: string[];
dimensions: string[];
brands: string[];
search: string;
}

19
main.py
View File

@ -16,6 +16,7 @@ FILTER_SQL = """
char_length(city) > 0
AND (length({cities:Array(String)}) = 0 OR city IN {cities:Array(String)})
AND (length({dimensions:Array(String)}) = 0 OR dimension IN {dimensions:Array(String)})
AND (length({brands:Array(String)}) = 0 OR brand IN {brands:Array(String)})
AND (
{search:String} = ''
OR positionCaseInsensitive(address, {search:String}) > 0
@ -41,10 +42,11 @@ async def ch_query(sql: str, params: dict):
return [json.loads(line) for line in text.splitlines()]
def parse_filters(city: str, dimension: str, search: str):
def parse_filters(city: str, dimension: str, brand: str, search: str):
cities = [c for c in city.split(",") if c] if city else []
dimensions = [d for d in dimension.split(",") if d] if dimension else []
return {"cities": cities, "dimensions": dimensions, "search": search or ""}
brands = [b for b in brand.split(",") if b] if brand else []
return {"cities": cities, "dimensions": dimensions, "brands": brands, "search": search or ""}
@app.get("/api/meta")
@ -57,15 +59,20 @@ async def meta():
"SELECT DISTINCT dimension FROM default.pf_board WHERE char_length(dimension) > 0 ORDER BY dimension",
{},
)
brands = await ch_query(
"SELECT DISTINCT brand FROM default.pf_board WHERE char_length(brand) > 0 ORDER BY brand",
{},
)
return {
"cities": [r["city"] for r in cities],
"dimensions": [r["dimension"] for r in dimensions],
"brands": [r["brand"] for r in brands],
}
@app.get("/api/boards")
async def boards(city: str = "", dimension: str = "", search: str = ""):
params = parse_filters(city, dimension, search)
async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
params = parse_filters(city, dimension, brand, search)
sql = f"""
SELECT
board_id,
@ -82,8 +89,8 @@ async def boards(city: str = "", dimension: str = "", search: str = ""):
@app.get("/api/bookings")
async def bookings(city: str = "", dimension: str = "", search: str = ""):
params = parse_filters(city, dimension, search)
async def bookings(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
params = parse_filters(city, dimension, brand, search)
sql = f"""
SELECT
board_id,