date filters: independent Дата начала (start_date>=) / Дата окончания (end_date<=), header fields under Город/Бренд; qualify pf_board.start_date/end_date to avoid alias shadowing
This commit is contained in:
parent
fe06cdab72
commit
66e501ef59
@ -17,26 +17,38 @@
|
||||
</div>
|
||||
|
||||
<!-- Group 2: fields -->
|
||||
<div class="hgroup">
|
||||
<div class="field">
|
||||
<label>Город</label>
|
||||
<div class="dropdown" id="city-dropdown">
|
||||
<button type="button" class="dropdown-btn" id="city-btn">
|
||||
<span id="city-btn-text">Все города</span>
|
||||
<span class="caret">▾</span>
|
||||
</button>
|
||||
<div class="dropdown-panel" id="city-panel"></div>
|
||||
<div class="hgroup hgroup-fields">
|
||||
<div class="fcol">
|
||||
<div class="field">
|
||||
<label>Город</label>
|
||||
<div class="dropdown" id="city-dropdown">
|
||||
<button type="button" class="dropdown-btn" id="city-btn">
|
||||
<span id="city-btn-text">Все города</span>
|
||||
<span class="caret">▾</span>
|
||||
</button>
|
||||
<div class="dropdown-panel" id="city-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="date-start">Дата начала (от)</label>
|
||||
<input type="date" id="date-start" />
|
||||
</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 class="fcol">
|
||||
<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 for="date-end">Дата окончания (до)</label>
|
||||
<input type="date" id="date-end" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -57,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group 3: checkboxes -->
|
||||
<!-- Group 3: checkboxes (2-column compact grid) -->
|
||||
<div class="hgroup hgroup-checks">
|
||||
<div class="field checkbox-field">
|
||||
<label for="show-brand"><input type="checkbox" id="show-brand" checked /> Бренд</label>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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)]!;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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). */
|
||||
|
||||
50
main.py
50
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user