+
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,