filters: add faceted Status (task_status) filter — meta.statuses, boards/bookings/map filtering (map marker visibility), UI dropdown under Size in filter grid

This commit is contained in:
aaverbitskiy 2026-07-27 19:40:26 +00:00
parent 053bf2587d
commit c7eae87660
5 changed files with 52 additions and 17 deletions

View File

@ -76,6 +76,16 @@
<label for="date-end">Дата окончания (до)</label>
<input type="date" id="date-end" />
</div>
<div class="field">
<label>Статус</label>
<div class="dropdown" id="status-dropdown">
<button type="button" class="dropdown-btn" id="status-btn">
<span id="status-btn-text">Все статусы</span>
<span class="caret"></span>
</button>
<div class="dropdown-panel" id="status-panel"></div>
</div>
</div>
<div class="field">
<label for="search">Поиск (адрес / код)</label>
<input type="text" id="search" placeholder="например, Азина" />

View File

@ -8,6 +8,7 @@ function buildQuery(f: Filters): string {
dimension: f.dimensions.join(','),
brand: f.brands.join(','),
manager: f.managers.join(','),
status: f.statuses.join(','),
search: f.search,
date_start: f.dateStart,
date_end: f.dateEnd,

View File

@ -45,6 +45,7 @@ const cityDropdown = createDropdown('city');
const brandDropdown = createDropdown('brand');
const dimensionDropdown = createDropdown('dimension');
const managerDropdown = createDropdown('manager');
const statusDropdown = createDropdown('status');
const view = createTimelineView(
{
timelineEl: el.timelineEl,
@ -78,6 +79,7 @@ function currentFilters(): Filters {
dimensions: dimensionDropdown.getSelected(),
brands: brandDropdown.getSelected(),
managers: managerDropdown.getSelected(),
statuses: statusDropdown.getSelected(),
search: el.search.value.trim(),
dateStart: el.dateStart.value,
dateEnd: el.dateEnd.value,
@ -116,6 +118,7 @@ async function loadData(fit: boolean): Promise<void> {
brandDropdown.updateValues(meta.brands);
dimensionDropdown.updateValues(meta.dimensions);
managerDropdown.updateValues(meta.managers);
statusDropdown.updateValues(meta.statuses);
lastBoards = boards;
lastBookings = bookings;
view.render(boards, bookings, fit, flags());
@ -131,7 +134,7 @@ async function loadData(fit: boolean): Promise<void> {
let mapLoadedKey = '';
function facetKey(): string {
const f = currentFilters();
return JSON.stringify([f.cities, f.dimensions, f.brands, f.search]);
return JSON.stringify([f.cities, f.dimensions, f.brands, f.managers, f.statuses, f.search]);
}
function mapVisible(): boolean {
return !!viewModes && viewModes.getMode() !== 'timeline';
@ -187,6 +190,7 @@ cityDropdown.onChange(scheduleReload);
brandDropdown.onChange(scheduleReload);
dimensionDropdown.onChange(scheduleReload);
managerDropdown.onChange(scheduleReload);
statusDropdown.onChange(scheduleReload);
el.search.addEventListener('input', scheduleReload);
el.dateStart.addEventListener('change', scheduleReload);
el.dateEnd.addEventListener('change', scheduleReload);
@ -202,6 +206,7 @@ async function init(): Promise<void> {
brandDropdown.setValues([], 'Все бренды');
dimensionDropdown.setValues([], 'Все размеры');
managerDropdown.setValues([], 'Все менеджеры');
statusDropdown.setValues([], 'Все статусы');
rebuildDecorMenu();
view.applyLayout();
await loadData(true);

View File

@ -8,6 +8,7 @@ export interface Meta {
dimensions: string[];
brands: string[];
managers: string[];
statuses: string[];
}
/** One row of GET /api/boards (grouped per board_id). */
@ -35,6 +36,7 @@ export interface Filters {
dimensions: string[];
brands: string[];
managers: string[];
statuses: string[];
search: string;
dateStart: string; // YYYY-MM-DD or '' — start_date >= dateStart
dateEnd: string; // YYYY-MM-DD or '' — end_date <= dateEnd

49
main.py
View File

@ -20,6 +20,7 @@ BRAND_COND = "(length({brands:Array(String)}) = 0 OR brand IN {brands:Array(Stri
# manager is Array(String) in pf_board (a task may have several managers), so we
# match with hasAny: keep the row if any selected manager is among its managers.
MANAGER_COND = "(length({managers:Array(String)}) = 0 OR hasAny(manager, {managers:Array(String)}))"
STATUS_COND = "(length({statuses:Array(String)}) = 0 OR task_status IN {statuses:Array(String)})"
SEARCH_COND = (
"({search:String} = ''"
" OR positionCaseInsensitive(address, {search:String}) > 0"
@ -46,9 +47,15 @@ MAP_MANAGER_COND = (
"(length({managers:Array(String)}) = 0"
" OR board_key IN (SELECT board_key FROM default.pf_board WHERE hasAny(manager, {managers:Array(String)})))"
)
# Same idea for the status facet on the map: keep surfaces that have at least one
# booking whose task_status is among the selected values.
MAP_STATUS_COND = (
"(length({statuses:Array(String)}) = 0"
" OR board_key IN (SELECT board_key FROM default.pf_board WHERE task_status IN {statuses:Array(String)}))"
)
def build_where(city=False, dimension=False, brand=False, manager=False, search=False, date_start=False, date_end=False):
def build_where(city=False, dimension=False, brand=False, manager=False, status=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).
@ -61,6 +68,8 @@ def build_where(city=False, dimension=False, brand=False, manager=False, search=
parts.append(BRAND_COND)
if manager:
parts.append(MANAGER_COND)
if status:
parts.append(STATUS_COND)
if search:
parts.append(SEARCH_COND)
if date_start:
@ -87,16 +96,18 @@ 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, manager: str, search: str, date_start: str, date_end: str):
def parse_filters(city: str, dimension: str, brand: str, manager: str, status: 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 []
managers = [m for m in manager.split(",") if m] if manager else []
statuses = [s for s in status.split(",") if s] if status else []
return {
"cities": cities,
"dimensions": dimensions,
"brands": brands,
"managers": managers,
"statuses": statuses,
"search": search or "",
"date_start": date_start or "",
"date_end": date_end or "",
@ -104,25 +115,29 @@ def parse_filters(city: str, dimension: str, brand: str, manager: str, search: s
@app.get("/api/meta")
async def meta(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, search, date_start, date_end)
async def meta(city: str = "", dimension: str = "", brand: str = "", manager: str = "", status: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, status, 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, manager=True, search=True, date_start=hds, date_end=hde)} AND char_length(city) > 0 ORDER BY city",
f"SELECT DISTINCT city FROM default.pf_board WHERE {build_where(dimension=True, brand=True, manager=True, status=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, manager=True, search=True, date_start=hds, date_end=hde)} AND char_length(brand) > 0 ORDER BY brand",
f"SELECT DISTINCT brand FROM default.pf_board WHERE {build_where(city=True, dimension=True, manager=True, status=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, manager=True, search=True, date_start=hds, date_end=hde)} AND char_length(dimension) > 0 ORDER BY dimension",
f"SELECT DISTINCT dimension FROM default.pf_board WHERE {build_where(city=True, brand=True, manager=True, status=True, search=True, date_start=hds, date_end=hde)} AND char_length(dimension) > 0 ORDER BY dimension",
params,
)
# manager is an Array(String) column — ARRAY JOIN to enumerate distinct names.
managers = await ch_query(
f"SELECT DISTINCT m AS manager FROM default.pf_board ARRAY JOIN manager AS m WHERE {build_where(city=True, dimension=True, brand=True, search=True, date_start=hds, date_end=hde)} AND char_length(m) > 0 ORDER BY manager",
f"SELECT DISTINCT m AS manager FROM default.pf_board ARRAY JOIN manager AS m WHERE {build_where(city=True, dimension=True, brand=True, status=True, search=True, date_start=hds, date_end=hde)} AND char_length(m) > 0 ORDER BY manager",
params,
)
statuses = await ch_query(
f"SELECT DISTINCT task_status FROM default.pf_board WHERE {build_where(city=True, dimension=True, brand=True, manager=True, search=True, date_start=hds, date_end=hde)} AND char_length(task_status) > 0 ORDER BY task_status",
params,
)
return {
@ -130,13 +145,14 @@ async def meta(city: str = "", dimension: str = "", brand: str = "", manager: st
"dimensions": [r["dimension"] for r in dimensions],
"brands": [r["brand"] for r in brands],
"managers": [r["manager"] for r in managers],
"statuses": [r["task_status"] for r in statuses],
}
@app.get("/api/boards")
async def boards(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, search, date_start, date_end)
where = build_where(city=True, dimension=True, brand=True, manager=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
async def boards(city: str = "", dimension: str = "", brand: str = "", manager: str = "", status: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, status, search, date_start, date_end)
where = build_where(city=True, dimension=True, brand=True, manager=True, status=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
sql = f"""
SELECT
board_id,
@ -153,9 +169,9 @@ async def boards(city: str = "", dimension: str = "", brand: str = "", manager:
@app.get("/api/bookings")
async def bookings(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, search, date_start, date_end)
where = build_where(city=True, dimension=True, brand=True, manager=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
async def bookings(city: str = "", dimension: str = "", brand: str = "", manager: str = "", status: str = "", search: str = "", date_start: str = "", date_end: str = ""):
params = parse_filters(city, dimension, brand, manager, status, search, date_start, date_end)
where = build_where(city=True, dimension=True, brand=True, manager=True, status=True, search=True, date_start=bool(params["date_start"]), date_end=bool(params["date_end"]))
sql = f"""
SELECT
board_id,
@ -174,11 +190,11 @@ async def bookings(city: str = "", dimension: str = "", brand: str = "", manager
@app.get("/api/map")
async def map_data(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = ""):
async def map_data(city: str = "", dimension: str = "", brand: str = "", manager: str = "", status: str = "", search: str = ""):
# Surfaces come from board_info (has coordinates). Occupancy is computed "as of
# today" and is intentionally NOT affected by the date-range filters — only the
# Город/Бренд/Размер/Менеджер/Поиск facets narrow which surfaces are shown.
params = parse_filters(city, dimension, brand, manager, search, "", "")
params = parse_filters(city, dimension, brand, manager, status, search, "", "")
where = " AND ".join([
"char_length(city) > 0",
CITY_COND,
@ -186,6 +202,7 @@ async def map_data(city: str = "", dimension: str = "", brand: str = "", manager
SEARCH_COND,
MAP_BRAND_COND,
MAP_MANAGER_COND,
MAP_STATUS_COND,
])
surfaces_sql = f"""
SELECT