5d60d33db1
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
183 lines
7.6 KiB
Python
183 lines
7.6 KiB
Python
"""City demographics and 360 entity workspace."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from app.connectors import cbs, pdok
|
|
from app.db import execute, execute_returning, fetch_all, fetch_one, json_param
|
|
|
|
|
|
def sync_city_demographics(limit: int = 100) -> dict[str, Any]:
|
|
cities = fetch_all(
|
|
"""SELECT DISTINCT city, province FROM supermarkets
|
|
WHERE city IS NOT NULL AND city <> 'Onbekend'
|
|
AND city NOT IN (SELECT city FROM city_demographics)
|
|
LIMIT %s""",
|
|
(limit,),
|
|
)
|
|
synced = 0
|
|
for row in cities:
|
|
pc_rows = fetch_all(
|
|
"SELECT postcode FROM supermarkets WHERE city = %s AND postcode <> '0000AA' LIMIT 1",
|
|
(row["city"],),
|
|
)
|
|
if not pc_rows:
|
|
continue
|
|
pd = pdok.lookup_postcode(pc_rows[0]["postcode"])
|
|
if not pd or not pd.get("municipality_code"):
|
|
continue
|
|
stats = cbs.fetch_gemeente_stats(pd["municipality_code"])
|
|
if not stats:
|
|
continue
|
|
rel = stats.get("religious_composition") or {}
|
|
execute_returning(
|
|
"""INSERT INTO city_demographics (city, province, gemeente_code, population, households,
|
|
avg_income, muslim_proxy_pct, data_source, last_updated)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,'cbs+pdok',NOW())
|
|
ON CONFLICT (city, province) DO UPDATE SET
|
|
population=EXCLUDED.population, households=EXCLUDED.households,
|
|
avg_income=EXCLUDED.avg_income, muslim_proxy_pct=EXCLUDED.muslim_proxy_pct,
|
|
last_updated=NOW() RETURNING id""",
|
|
(
|
|
row["city"], row.get("province") or pd.get("province"),
|
|
pd.get("municipality_code"), stats.get("population"), stats.get("households"),
|
|
stats.get("avg_income"), rel.get("muslim_proxy_pct"),
|
|
),
|
|
)
|
|
synced += 1
|
|
return {"synced": synced}
|
|
|
|
|
|
def get_city_context(city: str, province: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
if province:
|
|
row = fetch_one(
|
|
"SELECT * FROM city_demographics WHERE city ILIKE %s AND province ILIKE %s",
|
|
(city, province),
|
|
)
|
|
else:
|
|
row = fetch_one("SELECT * FROM city_demographics WHERE city ILIKE %s LIMIT 1", (city,))
|
|
if not row:
|
|
return None
|
|
stores = fetch_one(
|
|
"SELECT COUNT(*) AS n FROM supermarkets WHERE city ILIKE %s", (city,)
|
|
)
|
|
out = dict(row)
|
|
out["stores_in_city"] = int((stores or {}).get("n") or 0)
|
|
return out
|
|
|
|
|
|
def get_store_360(supermarket_id: int) -> dict[str, Any]:
|
|
store = fetch_one("SELECT * FROM supermarkets WHERE id = %s", (supermarket_id,))
|
|
if not store:
|
|
raise ValueError("Store not found")
|
|
city_ctx = get_city_context(store["city"], store.get("province"))
|
|
notes = fetch_all(
|
|
"SELECT * FROM entity_notes WHERE entity_type='supermarket' AND entity_id=%s ORDER BY pinned DESC, created_at DESC",
|
|
(supermarket_id,),
|
|
)
|
|
media = fetch_all(
|
|
"SELECT * FROM entity_media WHERE entity_type='supermarket' AND entity_id=%s ORDER BY created_at DESC",
|
|
(supermarket_id,),
|
|
)
|
|
milestones = fetch_all(
|
|
"SELECT * FROM sales_milestones WHERE supermarket_id=%s ORDER BY sort_order, target_date NULLS LAST",
|
|
(supermarket_id,),
|
|
)
|
|
ownership = fetch_all(
|
|
"""SELECT * FROM ownership_changes
|
|
WHERE (entity_id=%s AND entity_type='supermarket') OR chain ILIKE %s
|
|
ORDER BY effective_date DESC NULLS LAST LIMIT 10""",
|
|
(supermarket_id, f"%{store['chain']}%"),
|
|
)
|
|
calendar = fetch_all(
|
|
"""SELECT * FROM calendar_events WHERE supermarket_id=%s OR (client_id=%s AND client_id IS NOT NULL)
|
|
ORDER BY starts_at DESC LIMIT 10""",
|
|
(supermarket_id, store.get("client_id")),
|
|
)
|
|
nearby_count = fetch_one(
|
|
"SELECT COUNT(*) AS n FROM supermarkets WHERE city ILIKE %s AND id <> %s",
|
|
(store["city"], supermarket_id),
|
|
)
|
|
area = fetch_one("SELECT * FROM area_analysis WHERE postcode = %s", (store.get("postcode"),))
|
|
weather = fetch_all(
|
|
"SELECT * FROM weather_data WHERE city ILIKE %s ORDER BY date DESC LIMIT 7",
|
|
(f"%{store.get('city', '')}%",),
|
|
)
|
|
return {
|
|
"store": dict(store),
|
|
"city": city_ctx,
|
|
"catchment": {
|
|
"city_population": (city_ctx or {}).get("population"),
|
|
"stores_in_city": int((nearby_count or {}).get("n") or 0),
|
|
"gemeente_population": (area or {}).get("population"),
|
|
"postcode_population_proxy": (area or {}).get("population"),
|
|
},
|
|
"notes": [dict(n) for n in notes],
|
|
"media": [dict(m) for m in media],
|
|
"milestones": [dict(m) for m in milestones],
|
|
"ownership_changes": [dict(o) for o in ownership],
|
|
"calendar": [dict(c) for c in calendar],
|
|
"area_analysis": dict(area) if area else None,
|
|
"weather": [dict(w) for w in weather],
|
|
}
|
|
|
|
|
|
def add_note(entity_type: str, entity_id: int, body: str, title: Optional[str] = None, note_type: str = "general") -> dict[str, Any]:
|
|
row = execute_returning(
|
|
"""INSERT INTO entity_notes (entity_type, entity_id, title, body, note_type)
|
|
VALUES (%s,%s,%s,%s,%s) RETURNING *""",
|
|
(entity_type, entity_id, title, body, note_type),
|
|
)
|
|
return dict(row or {})
|
|
|
|
|
|
def add_milestone(supermarket_id: int, title: str, milestone_type: str = "custom", **kwargs: Any) -> dict[str, Any]:
|
|
row = execute_returning(
|
|
"""INSERT INTO sales_milestones (supermarket_id, client_id, deal_id, milestone_type, title,
|
|
status, target_date, value_eur, notes, sort_order)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING *""",
|
|
(
|
|
supermarket_id, kwargs.get("client_id"), kwargs.get("deal_id"), milestone_type, title,
|
|
kwargs.get("status", "pending"), kwargs.get("target_date"), kwargs.get("value_eur"),
|
|
kwargs.get("notes"), kwargs.get("sort_order", 0),
|
|
),
|
|
)
|
|
return dict(row or {})
|
|
|
|
|
|
def add_ownership(**kwargs: Any) -> dict[str, Any]:
|
|
row = execute_returning(
|
|
"""INSERT INTO ownership_changes (entity_type, entity_id, chain, previous_owner, new_owner,
|
|
change_type, effective_date, source, notes)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) RETURNING *""",
|
|
(
|
|
kwargs.get("entity_type", "supermarket"), kwargs.get("entity_id"), kwargs.get("chain"),
|
|
kwargs.get("previous_owner"), kwargs["new_owner"], kwargs.get("change_type", "acquisition"),
|
|
kwargs.get("effective_date"), kwargs.get("source"), kwargs.get("notes"),
|
|
),
|
|
)
|
|
return dict(row or {})
|
|
|
|
|
|
def add_calendar_event(supermarket_id: int, title: str, starts_at: str, **kwargs: Any) -> dict[str, Any]:
|
|
row = execute_returning(
|
|
"""INSERT INTO calendar_events (title, description, starts_at, ends_at, client_id, deal_id,
|
|
supermarket_id, location, source)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'retail_360') RETURNING *""",
|
|
(
|
|
title, kwargs.get("description"), starts_at, kwargs.get("ends_at"),
|
|
kwargs.get("client_id"), kwargs.get("deal_id"), supermarket_id,
|
|
kwargs.get("location"),
|
|
),
|
|
)
|
|
return dict(row or {})
|
|
|
|
|
|
def register_media(entity_type: str, entity_id: int, filename: str, storage_path: str, content_type: str, caption: Optional[str] = None) -> dict[str, Any]:
|
|
row = execute_returning(
|
|
"""INSERT INTO entity_media (entity_type, entity_id, filename, storage_path, content_type, caption)
|
|
VALUES (%s,%s,%s,%s,%s,%s) RETURNING *""",
|
|
(entity_type, entity_id, filename, storage_path, content_type, caption),
|
|
)
|
|
return dict(row or {})
|