diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index cc0710c..9f4c8d1 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -8,6 +8,8 @@ function buildQuery(f: Filters): string {
dimension: f.dimensions.join(','),
brand: f.brands.join(','),
search: f.search,
+ date_start: f.dateStart,
+ date_end: f.dateEnd,
});
return params.toString();
}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 78170e9..a45a7f7 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -9,6 +9,8 @@ import { loadAppearance, applyCssVars, buildAppearanceMenu } from './appearance'
const el = {
search: document.getElementById('search') as HTMLInputElement,
+ dateStart: document.getElementById('date-start') as HTMLInputElement,
+ dateEnd: document.getElementById('date-end') as HTMLInputElement,
status: document.getElementById('status') as HTMLElement,
timelineEl: document.getElementById('timeline') as HTMLElement,
empty: document.getElementById('empty') as HTMLElement,
@@ -58,6 +60,8 @@ function currentFilters(): Filters {
dimensions: dimensionDropdown.getSelected(),
brands: brandDropdown.getSelected(),
search: el.search.value.trim(),
+ dateStart: el.dateStart.value,
+ dateEnd: el.dateEnd.value,
};
}
@@ -114,6 +118,8 @@ cityDropdown.onChange(scheduleReload);
brandDropdown.onChange(scheduleReload);
dimensionDropdown.onChange(scheduleReload);
el.search.addEventListener('input', scheduleReload);
+el.dateStart.addEventListener('change', scheduleReload);
+el.dateEnd.addEventListener('change', scheduleReload);
el.zoomSlider.addEventListener('input', () => {
const step = zoomSteps[parseInt(el.zoomSlider.value, 10)]!;
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index bed9a8e..3f624a8 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -45,6 +45,17 @@ header {
border-radius: 8px;
}
.hgroup-brand { background: #eaeaea; }
+/* Fields group: top-align so the date input sits UNDER Город/Бренд. */
+.hgroup-fields { align-items: flex-start; }
+.fcol { display: flex; flex-direction: column; gap: 8px; }
+input[type=date] {
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-size: 13px;
+ min-width: 180px;
+ box-sizing: border-box;
+}
/* Compact 2-column layout for the checkbox group. */
.hgroup-checks {
display: grid;
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 9586a25..1077eca 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -34,6 +34,8 @@ export interface Filters {
dimensions: string[];
brands: string[];
search: string;
+ dateStart: string; // YYYY-MM-DD or '' — start_date >= dateStart
+ dateEnd: string; // YYYY-MM-DD or '' — end_date <= dateEnd
}
/** task_status value that marks an archived booking (painted slate #708090). */
diff --git a/main.py b/main.py
index 2b81bdb..d2016af 100644
--- a/main.py
+++ b/main.py
@@ -20,9 +20,13 @@ SEARCH_COND = (
" OR positionCaseInsensitive(address, {search:String}) > 0"
" OR positionCaseInsensitive(board_id, {search:String}) > 0)"
)
+# Independent date bounds: start_date >= X ("Дата начала"), end_date <= Y
+# ("Дата окончания"). Each included only when its value is provided.
+START_COND = "pf_board.start_date >= {date_start:Date}"
+END_COND = "pf_board.end_date <= {date_end:Date}"
-def build_where(city=False, dimension=False, brand=False, search=False):
+def build_where(city=False, dimension=False, brand=False, search=False, date_start=False, date_end=False):
# Faceted WHERE: include only the requested facet conditions. For the
# dependent-filter option lists we exclude a facet's own condition so its
# available values reflect the OTHER filters (all-but-self).
@@ -35,12 +39,13 @@ def build_where(city=False, dimension=False, brand=False, search=False):
parts.append(BRAND_COND)
if search:
parts.append(SEARCH_COND)
+ if date_start:
+ parts.append(START_COND)
+ if date_end:
+ parts.append(END_COND)
return "\n AND ".join(parts)
-WHERE_ALL = build_where(city=True, dimension=True, brand=True, search=True)
-
-
async def ch_query(sql: str, params: dict):
form = {"query": sql, "default_format": "JSONEachRow"}
for k, v in params.items():
@@ -58,26 +63,35 @@ async def ch_query(sql: str, params: dict):
return [json.loads(line) for line in text.splitlines()]
-def parse_filters(city: str, dimension: str, brand: str, search: str):
+def parse_filters(city: str, dimension: str, brand: str, search: str, date_start: str, date_end: 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 []
brands = [b for b in brand.split(",") if b] if brand else []
- return {"cities": cities, "dimensions": dimensions, "brands": brands, "search": search or ""}
+ return {
+ "cities": cities,
+ "dimensions": dimensions,
+ "brands": brands,
+ "search": search or "",
+ "date_start": date_start or "",
+ "date_end": date_end or "",
+ }
@app.get("/api/meta")
-async def meta(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
- params = parse_filters(city, dimension, brand, search)
+async def meta(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
+ params = parse_filters(city, dimension, brand, search, date_start, date_end)
+ hds = bool(params["date_start"])
+ hde = bool(params["date_end"])
cities = await ch_query(
- f"SELECT DISTINCT city FROM default.pf_board WHERE {build_where(dimension=True, brand=True, search=True)} AND char_length(city) > 0 ORDER BY city",
+ f"SELECT DISTINCT city FROM default.pf_board WHERE {build_where(dimension=True, brand=True, search=True, date_start=hds, date_end=hde)} AND char_length(city) > 0 ORDER BY city",
params,
)
brands = await ch_query(
- f"SELECT DISTINCT brand FROM default.pf_board WHERE {build_where(city=True, dimension=True, search=True)} AND char_length(brand) > 0 ORDER BY brand",
+ f"SELECT DISTINCT brand FROM default.pf_board WHERE {build_where(city=True, dimension=True, search=True, date_start=hds, date_end=hde)} AND char_length(brand) > 0 ORDER BY brand",
params,
)
dimensions = await ch_query(
- f"SELECT DISTINCT dimension FROM default.pf_board WHERE {build_where(city=True, brand=True, search=True)} AND char_length(dimension) > 0 ORDER BY dimension",
+ f"SELECT DISTINCT dimension FROM default.pf_board WHERE {build_where(city=True, brand=True, search=True, date_start=hds, date_end=hde)} AND char_length(dimension) > 0 ORDER BY dimension",
params,
)
return {
@@ -88,8 +102,9 @@ async def meta(city: str = "", dimension: str = "", brand: str = "", search: str
@app.get("/api/boards")
-async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
- params = parse_filters(city, dimension, brand, search)
+async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
+ params = parse_filters(city, dimension, brand, search, date_start, date_end)
+ where = build_where(city=True, dimension=True, brand=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
sql = f"""
SELECT
board_id,
@@ -97,7 +112,7 @@ async def boards(city: str = "", dimension: str = "", brand: str = "", search: s
argMax(city, last_activity) AS board_city,
argMax(dimension, last_activity) AS board_dimension
FROM default.pf_board
- WHERE {WHERE_ALL}
+ WHERE {where}
GROUP BY board_id
ORDER BY board_address
"""
@@ -106,8 +121,9 @@ async def boards(city: str = "", dimension: str = "", brand: str = "", search: s
@app.get("/api/bookings")
-async def bookings(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
- params = parse_filters(city, dimension, brand, search)
+async def bookings(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
+ params = parse_filters(city, dimension, brand, search, date_start, date_end)
+ where = build_where(city=True, dimension=True, brand=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
sql = f"""
SELECT
board_id,
@@ -118,7 +134,7 @@ async def bookings(city: str = "", dimension: str = "", brand: str = "", search:
brand,
company_name
FROM default.pf_board
- WHERE {WHERE_ALL}
+ WHERE {where}
ORDER BY board_id, start_date
"""
rows = await ch_query(sql, params)