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:
parent
053bf2587d
commit
c7eae87660
@ -76,6 +76,16 @@
|
|||||||
<label for="date-end">Дата окончания (до)</label>
|
<label for="date-end">Дата окончания (до)</label>
|
||||||
<input type="date" id="date-end" />
|
<input type="date" id="date-end" />
|
||||||
</div>
|
</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">
|
<div class="field">
|
||||||
<label for="search">Поиск (адрес / код)</label>
|
<label for="search">Поиск (адрес / код)</label>
|
||||||
<input type="text" id="search" placeholder="например, Азина" />
|
<input type="text" id="search" placeholder="например, Азина" />
|
||||||
|
|||||||
@ -8,6 +8,7 @@ function buildQuery(f: Filters): string {
|
|||||||
dimension: f.dimensions.join(','),
|
dimension: f.dimensions.join(','),
|
||||||
brand: f.brands.join(','),
|
brand: f.brands.join(','),
|
||||||
manager: f.managers.join(','),
|
manager: f.managers.join(','),
|
||||||
|
status: f.statuses.join(','),
|
||||||
search: f.search,
|
search: f.search,
|
||||||
date_start: f.dateStart,
|
date_start: f.dateStart,
|
||||||
date_end: f.dateEnd,
|
date_end: f.dateEnd,
|
||||||
|
|||||||
@ -45,6 +45,7 @@ const cityDropdown = createDropdown('city');
|
|||||||
const brandDropdown = createDropdown('brand');
|
const brandDropdown = createDropdown('brand');
|
||||||
const dimensionDropdown = createDropdown('dimension');
|
const dimensionDropdown = createDropdown('dimension');
|
||||||
const managerDropdown = createDropdown('manager');
|
const managerDropdown = createDropdown('manager');
|
||||||
|
const statusDropdown = createDropdown('status');
|
||||||
const view = createTimelineView(
|
const view = createTimelineView(
|
||||||
{
|
{
|
||||||
timelineEl: el.timelineEl,
|
timelineEl: el.timelineEl,
|
||||||
@ -78,6 +79,7 @@ function currentFilters(): Filters {
|
|||||||
dimensions: dimensionDropdown.getSelected(),
|
dimensions: dimensionDropdown.getSelected(),
|
||||||
brands: brandDropdown.getSelected(),
|
brands: brandDropdown.getSelected(),
|
||||||
managers: managerDropdown.getSelected(),
|
managers: managerDropdown.getSelected(),
|
||||||
|
statuses: statusDropdown.getSelected(),
|
||||||
search: el.search.value.trim(),
|
search: el.search.value.trim(),
|
||||||
dateStart: el.dateStart.value,
|
dateStart: el.dateStart.value,
|
||||||
dateEnd: el.dateEnd.value,
|
dateEnd: el.dateEnd.value,
|
||||||
@ -116,6 +118,7 @@ async function loadData(fit: boolean): Promise<void> {
|
|||||||
brandDropdown.updateValues(meta.brands);
|
brandDropdown.updateValues(meta.brands);
|
||||||
dimensionDropdown.updateValues(meta.dimensions);
|
dimensionDropdown.updateValues(meta.dimensions);
|
||||||
managerDropdown.updateValues(meta.managers);
|
managerDropdown.updateValues(meta.managers);
|
||||||
|
statusDropdown.updateValues(meta.statuses);
|
||||||
lastBoards = boards;
|
lastBoards = boards;
|
||||||
lastBookings = bookings;
|
lastBookings = bookings;
|
||||||
view.render(boards, bookings, fit, flags());
|
view.render(boards, bookings, fit, flags());
|
||||||
@ -131,7 +134,7 @@ async function loadData(fit: boolean): Promise<void> {
|
|||||||
let mapLoadedKey = '';
|
let mapLoadedKey = '';
|
||||||
function facetKey(): string {
|
function facetKey(): string {
|
||||||
const f = currentFilters();
|
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 {
|
function mapVisible(): boolean {
|
||||||
return !!viewModes && viewModes.getMode() !== 'timeline';
|
return !!viewModes && viewModes.getMode() !== 'timeline';
|
||||||
@ -187,6 +190,7 @@ cityDropdown.onChange(scheduleReload);
|
|||||||
brandDropdown.onChange(scheduleReload);
|
brandDropdown.onChange(scheduleReload);
|
||||||
dimensionDropdown.onChange(scheduleReload);
|
dimensionDropdown.onChange(scheduleReload);
|
||||||
managerDropdown.onChange(scheduleReload);
|
managerDropdown.onChange(scheduleReload);
|
||||||
|
statusDropdown.onChange(scheduleReload);
|
||||||
el.search.addEventListener('input', scheduleReload);
|
el.search.addEventListener('input', scheduleReload);
|
||||||
el.dateStart.addEventListener('change', scheduleReload);
|
el.dateStart.addEventListener('change', scheduleReload);
|
||||||
el.dateEnd.addEventListener('change', scheduleReload);
|
el.dateEnd.addEventListener('change', scheduleReload);
|
||||||
@ -202,6 +206,7 @@ async function init(): Promise<void> {
|
|||||||
brandDropdown.setValues([], 'Все бренды');
|
brandDropdown.setValues([], 'Все бренды');
|
||||||
dimensionDropdown.setValues([], 'Все размеры');
|
dimensionDropdown.setValues([], 'Все размеры');
|
||||||
managerDropdown.setValues([], 'Все менеджеры');
|
managerDropdown.setValues([], 'Все менеджеры');
|
||||||
|
statusDropdown.setValues([], 'Все статусы');
|
||||||
rebuildDecorMenu();
|
rebuildDecorMenu();
|
||||||
view.applyLayout();
|
view.applyLayout();
|
||||||
await loadData(true);
|
await loadData(true);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface Meta {
|
|||||||
dimensions: string[];
|
dimensions: string[];
|
||||||
brands: string[];
|
brands: string[];
|
||||||
managers: string[];
|
managers: string[];
|
||||||
|
statuses: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One row of GET /api/boards (grouped per board_id). */
|
/** One row of GET /api/boards (grouped per board_id). */
|
||||||
@ -35,6 +36,7 @@ export interface Filters {
|
|||||||
dimensions: string[];
|
dimensions: string[];
|
||||||
brands: string[];
|
brands: string[];
|
||||||
managers: string[];
|
managers: string[];
|
||||||
|
statuses: string[];
|
||||||
search: string;
|
search: string;
|
||||||
dateStart: string; // YYYY-MM-DD or '' — start_date >= dateStart
|
dateStart: string; // YYYY-MM-DD or '' — start_date >= dateStart
|
||||||
dateEnd: string; // YYYY-MM-DD or '' — end_date <= dateEnd
|
dateEnd: string; // YYYY-MM-DD or '' — end_date <= dateEnd
|
||||||
|
|||||||
49
main.py
49
main.py
@ -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
|
# 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.
|
# 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)}))"
|
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_COND = (
|
||||||
"({search:String} = ''"
|
"({search:String} = ''"
|
||||||
" OR positionCaseInsensitive(address, {search:String}) > 0"
|
" OR positionCaseInsensitive(address, {search:String}) > 0"
|
||||||
@ -46,9 +47,15 @@ MAP_MANAGER_COND = (
|
|||||||
"(length({managers:Array(String)}) = 0"
|
"(length({managers:Array(String)}) = 0"
|
||||||
" OR board_key IN (SELECT board_key FROM default.pf_board WHERE hasAny(manager, {managers:Array(String)})))"
|
" 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
|
# 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).
|
||||||
@ -61,6 +68,8 @@ def build_where(city=False, dimension=False, brand=False, manager=False, search=
|
|||||||
parts.append(BRAND_COND)
|
parts.append(BRAND_COND)
|
||||||
if manager:
|
if manager:
|
||||||
parts.append(MANAGER_COND)
|
parts.append(MANAGER_COND)
|
||||||
|
if status:
|
||||||
|
parts.append(STATUS_COND)
|
||||||
if search:
|
if search:
|
||||||
parts.append(SEARCH_COND)
|
parts.append(SEARCH_COND)
|
||||||
if date_start:
|
if date_start:
|
||||||
@ -87,16 +96,18 @@ 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, 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 []
|
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 []
|
||||||
managers = [m for m in manager.split(",") if m] if manager 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 {
|
return {
|
||||||
"cities": cities,
|
"cities": cities,
|
||||||
"dimensions": dimensions,
|
"dimensions": dimensions,
|
||||||
"brands": brands,
|
"brands": brands,
|
||||||
"managers": managers,
|
"managers": managers,
|
||||||
|
"statuses": statuses,
|
||||||
"search": search or "",
|
"search": search or "",
|
||||||
"date_start": date_start or "",
|
"date_start": date_start or "",
|
||||||
"date_end": date_end 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")
|
@app.get("/api/meta")
|
||||||
async def meta(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
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, search, date_start, date_end)
|
params = parse_filters(city, dimension, brand, manager, status, search, date_start, date_end)
|
||||||
hds = bool(params["date_start"])
|
hds = bool(params["date_start"])
|
||||||
hde = bool(params["date_end"])
|
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, 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,
|
params,
|
||||||
)
|
)
|
||||||
brands = await ch_query(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
dimensions = await ch_query(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
# manager is an Array(String) column — ARRAY JOIN to enumerate distinct names.
|
# manager is an Array(String) column — ARRAY JOIN to enumerate distinct names.
|
||||||
managers = await ch_query(
|
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,
|
params,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@ -130,13 +145,14 @@ async def meta(city: str = "", dimension: str = "", brand: str = "", manager: st
|
|||||||
"dimensions": [r["dimension"] for r in dimensions],
|
"dimensions": [r["dimension"] for r in dimensions],
|
||||||
"brands": [r["brand"] for r in brands],
|
"brands": [r["brand"] for r in brands],
|
||||||
"managers": [r["manager"] for r in managers],
|
"managers": [r["manager"] for r in managers],
|
||||||
|
"statuses": [r["task_status"] for r in statuses],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/boards")
|
@app.get("/api/boards")
|
||||||
async def boards(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
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, search, date_start, date_end)
|
params = parse_filters(city, dimension, brand, manager, status, 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"]))
|
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"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
board_id,
|
board_id,
|
||||||
@ -153,9 +169,9 @@ async def boards(city: str = "", dimension: str = "", brand: str = "", manager:
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/bookings")
|
@app.get("/api/bookings")
|
||||||
async def bookings(city: str = "", dimension: str = "", brand: str = "", manager: str = "", search: str = "", date_start: str = "", date_end: str = ""):
|
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, search, date_start, date_end)
|
params = parse_filters(city, dimension, brand, manager, status, 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"]))
|
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"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
board_id,
|
board_id,
|
||||||
@ -174,11 +190,11 @@ async def bookings(city: str = "", dimension: str = "", brand: str = "", manager
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/map")
|
@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
|
# 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
|
# today" and is intentionally NOT affected by the date-range filters — only the
|
||||||
# Город/Бренд/Размер/Менеджер/Поиск facets narrow which surfaces are shown.
|
# Город/Бренд/Размер/Менеджер/Поиск 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([
|
where = " AND ".join([
|
||||||
"char_length(city) > 0",
|
"char_length(city) > 0",
|
||||||
CITY_COND,
|
CITY_COND,
|
||||||
@ -186,6 +202,7 @@ async def map_data(city: str = "", dimension: str = "", brand: str = "", manager
|
|||||||
SEARCH_COND,
|
SEARCH_COND,
|
||||||
MAP_BRAND_COND,
|
MAP_BRAND_COND,
|
||||||
MAP_MANAGER_COND,
|
MAP_MANAGER_COND,
|
||||||
|
MAP_STATUS_COND,
|
||||||
])
|
])
|
||||||
surfaces_sql = f"""
|
surfaces_sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user