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,7 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 2: fields -->
|
<!-- Group 2: fields -->
|
||||||
<div class="hgroup">
|
<div class="hgroup hgroup-fields">
|
||||||
|
<div class="fcol">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Город</label>
|
<label>Город</label>
|
||||||
<div class="dropdown" id="city-dropdown">
|
<div class="dropdown" id="city-dropdown">
|
||||||
@ -28,7 +29,13 @@
|
|||||||
<div class="dropdown-panel" id="city-panel"></div>
|
<div class="dropdown-panel" id="city-panel"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="date-start">Дата начала (от)</label>
|
||||||
|
<input type="date" id="date-start" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fcol">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Бренд</label>
|
<label>Бренд</label>
|
||||||
<div class="dropdown" id="brand-dropdown">
|
<div class="dropdown" id="brand-dropdown">
|
||||||
@ -39,6 +46,11 @@
|
|||||||
<div class="dropdown-panel" id="brand-panel"></div>
|
<div class="dropdown-panel" id="brand-panel"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="date-end">Дата окончания (до)</label>
|
||||||
|
<input type="date" id="date-end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Размер</label>
|
<label>Размер</label>
|
||||||
@ -57,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 3: checkboxes -->
|
<!-- Group 3: checkboxes (2-column compact grid) -->
|
||||||
<div class="hgroup hgroup-checks">
|
<div class="hgroup hgroup-checks">
|
||||||
<div class="field checkbox-field">
|
<div class="field checkbox-field">
|
||||||
<label for="show-brand"><input type="checkbox" id="show-brand" checked /> Бренд</label>
|
<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(','),
|
dimension: f.dimensions.join(','),
|
||||||
brand: f.brands.join(','),
|
brand: f.brands.join(','),
|
||||||
search: f.search,
|
search: f.search,
|
||||||
|
date_start: f.dateStart,
|
||||||
|
date_end: f.dateEnd,
|
||||||
});
|
});
|
||||||
return params.toString();
|
return params.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import { loadAppearance, applyCssVars, buildAppearanceMenu } from './appearance'
|
|||||||
|
|
||||||
const el = {
|
const el = {
|
||||||
search: document.getElementById('search') as HTMLInputElement,
|
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,
|
status: document.getElementById('status') as HTMLElement,
|
||||||
timelineEl: document.getElementById('timeline') as HTMLElement,
|
timelineEl: document.getElementById('timeline') as HTMLElement,
|
||||||
empty: document.getElementById('empty') as HTMLElement,
|
empty: document.getElementById('empty') as HTMLElement,
|
||||||
@ -58,6 +60,8 @@ function currentFilters(): Filters {
|
|||||||
dimensions: dimensionDropdown.getSelected(),
|
dimensions: dimensionDropdown.getSelected(),
|
||||||
brands: brandDropdown.getSelected(),
|
brands: brandDropdown.getSelected(),
|
||||||
search: el.search.value.trim(),
|
search: el.search.value.trim(),
|
||||||
|
dateStart: el.dateStart.value,
|
||||||
|
dateEnd: el.dateEnd.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +118,8 @@ cityDropdown.onChange(scheduleReload);
|
|||||||
brandDropdown.onChange(scheduleReload);
|
brandDropdown.onChange(scheduleReload);
|
||||||
dimensionDropdown.onChange(scheduleReload);
|
dimensionDropdown.onChange(scheduleReload);
|
||||||
el.search.addEventListener('input', scheduleReload);
|
el.search.addEventListener('input', scheduleReload);
|
||||||
|
el.dateStart.addEventListener('change', scheduleReload);
|
||||||
|
el.dateEnd.addEventListener('change', scheduleReload);
|
||||||
|
|
||||||
el.zoomSlider.addEventListener('input', () => {
|
el.zoomSlider.addEventListener('input', () => {
|
||||||
const step = zoomSteps[parseInt(el.zoomSlider.value, 10)]!;
|
const step = zoomSteps[parseInt(el.zoomSlider.value, 10)]!;
|
||||||
|
|||||||
@ -45,6 +45,17 @@ header {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
.hgroup-brand { background: #eaeaea; }
|
.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. */
|
/* Compact 2-column layout for the checkbox group. */
|
||||||
.hgroup-checks {
|
.hgroup-checks {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export interface Filters {
|
|||||||
dimensions: string[];
|
dimensions: string[];
|
||||||
brands: string[];
|
brands: string[];
|
||||||
search: 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). */
|
/** 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(address, {search:String}) > 0"
|
||||||
" OR positionCaseInsensitive(board_id, {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
|
# Faceted WHERE: include only the requested facet conditions. For the
|
||||||
# dependent-filter option lists we exclude a facet's own condition so its
|
# dependent-filter option lists we exclude a facet's own condition so its
|
||||||
# available values reflect the OTHER filters (all-but-self).
|
# 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)
|
parts.append(BRAND_COND)
|
||||||
if search:
|
if search:
|
||||||
parts.append(SEARCH_COND)
|
parts.append(SEARCH_COND)
|
||||||
|
if date_start:
|
||||||
|
parts.append(START_COND)
|
||||||
|
if date_end:
|
||||||
|
parts.append(END_COND)
|
||||||
return "\n AND ".join(parts)
|
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):
|
async def ch_query(sql: str, params: dict):
|
||||||
form = {"query": sql, "default_format": "JSONEachRow"}
|
form = {"query": sql, "default_format": "JSONEachRow"}
|
||||||
for k, v in params.items():
|
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()]
|
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 []
|
cities = [c for c in city.split(",") if c] if city else []
|
||||||
dimensions = [d for d in dimension.split(",") if d] if dimension 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 []
|
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")
|
@app.get("/api/meta")
|
||||||
async def meta(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
|
async def meta(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
||||||
params = parse_filters(city, dimension, brand, search)
|
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(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
brands = await ch_query(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
dimensions = await ch_query(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@ -88,8 +102,9 @@ async def meta(city: str = "", dimension: str = "", brand: str = "", search: str
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/boards")
|
@app.get("/api/boards")
|
||||||
async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
|
async def boards(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
||||||
params = parse_filters(city, dimension, brand, search)
|
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"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
board_id,
|
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(city, last_activity) AS board_city,
|
||||||
argMax(dimension, last_activity) AS board_dimension
|
argMax(dimension, last_activity) AS board_dimension
|
||||||
FROM default.pf_board
|
FROM default.pf_board
|
||||||
WHERE {WHERE_ALL}
|
WHERE {where}
|
||||||
GROUP BY board_id
|
GROUP BY board_id
|
||||||
ORDER BY board_address
|
ORDER BY board_address
|
||||||
"""
|
"""
|
||||||
@ -106,8 +121,9 @@ async def boards(city: str = "", dimension: str = "", brand: str = "", search: s
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/bookings")
|
@app.get("/api/bookings")
|
||||||
async def bookings(city: str = "", dimension: str = "", brand: str = "", search: str = ""):
|
async def bookings(city: str = "", dimension: str = "", brand: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
||||||
params = parse_filters(city, dimension, brand, search)
|
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"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
board_id,
|
board_id,
|
||||||
@ -118,7 +134,7 @@ async def bookings(city: str = "", dimension: str = "", brand: str = "", search:
|
|||||||
brand,
|
brand,
|
||||||
company_name
|
company_name
|
||||||
FROM default.pf_board
|
FROM default.pf_board
|
||||||
WHERE {WHERE_ALL}
|
WHERE {where}
|
||||||
ORDER BY board_id, start_date
|
ORDER BY board_id, start_date
|
||||||
"""
|
"""
|
||||||
rows = await ch_query(sql, params)
|
rows = await ch_query(sql, params)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user