diff --git a/frontend/index.html b/frontend/index.html index a33978f..d73939f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -29,6 +29,17 @@ +
+ + +
+
-
+
- +
- +
diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1282bdf..8343b49 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -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(); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 6b846c1..23ac3c2 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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 { 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 { lastBoards = boards; lastBookings = bookings; view.render(boards, bookings, fit, flags()); - el.status.textContent = `Бордов: ${boards.length}, бронирований: ${bookings.length}`; + el.status.innerHTML = `Бордов: ${boards.length}
Бронирований: ${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); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ca5eee0..bed9a8e 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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 { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 03e4ca0..9586a25 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -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; } diff --git a/main.py b/main.py index bca159e..bad6553 100644 --- a/main.py +++ b/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,