diff --git a/frontend/index.html b/frontend/index.html
index 9de3d74..c08ab9f 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -76,6 +76,16 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index e2480fd..c3fbe7d 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -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,
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 47f4272..d20db05 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -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 {
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 {
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 {
brandDropdown.setValues([], 'Все бренды');
dimensionDropdown.setValues([], 'Все размеры');
managerDropdown.setValues([], 'Все менеджеры');
+ statusDropdown.setValues([], 'Все статусы');
rebuildDecorMenu();
view.applyLayout();
await loadData(true);
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index c1e60f2..91613a9 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -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
diff --git a/main.py b/main.py
index a54eea7..b78d354 100644
--- a/main.py
+++ b/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
# 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