brand filter (backend+frontend), compact checkbox grid, two-line counter
This commit is contained in:
parent
b393562c3a
commit
5db77b7a06
@ -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>
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
19
main.py
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user